VPS Tutorials
VPS Docker Container Keeps Restarting (2026): Logs, Exit Codes, Health Checks, and OOM Troubleshooting Checklist
When a Docker container on a VPS keeps restarting, it's usually not Docker itself that's broken, but rather the application failing to start, missing configuration, port conflicts, failed health checks, or OOM. Troubleshooting in the order of logs, exit codes, restart policy, resources, and dependencies lets you quickly pinpoint the cause.

VPS Docker Container Keeps Restarting (2026): Logs, Exit Codes, Health Checks, and OOM Troubleshooting Checklist
The most common scenario I've seen when a Docker container keeps Restarting is this:
docker ps
In the output, some service repeatedly becomes:
Restarting (1) 12 seconds ago
Restarting (137) 8 seconds ago
Restarting ( unhealthy )
At this point many people's first reaction is to restart Docker:
systemctl restart docker
Sometimes this does temporarily relieve things, but most of the time it doesn't help. Because a container that keeps restarting usually isn't a broken Docker engine, but rather the main process inside the container exiting immediately after starting.
In other words, Docker is just repeatedly pulling it up according to your restart policy. The real problem is usually hidden in:
- Application startup errors
- Missing environment variables
- Wrong configuration file paths
- Ports already in use
- Database not connected
- Incorrect file permissions
- Not enough memory, killed by OOM
- healthcheck written incorrectly
This article follows the order I usually use when troubleshooting, with no fluff—just commands and how to judge.
The first command isn't restart, but:
docker ps -a
Be sure to add -a. Otherwise you may not see containers that have already exited.
Focus on these columns:
STATUS
NAMES
COMMAND
Common states include:
| State | Rough meaning |
|---|---|
Exited (1) | The application itself errored and exited, most common |
Exited (0) | The main process ended normally, but isn't suitable as a long-running service |
Exited (137) | Very likely killed by OOM or received SIGKILL |
Restarting (1) | Repeatedly pulled up by the restart policy after failing to start |
Restarting (137) | Killed after each start, check memory first |
If you see Restarting (1), don't guess—look at the logs directly.
The most commonly used is:
docker logs container_name
But when a container keeps restarting, logs may scroll very fast. I'd recommend looking at it like this first:
docker logs --tail=200 container_name
If you want to observe continuously:
docker logs -f --tail=100 container_name
If you use Docker Compose:
docker compose logs --tail=200 service_name
Or look at all services:
docker compose logs --tail=200
Many problems are written directly in the logs:
Error: Cannot find module
permission denied
connect ECONNREFUSED 172.18.0.3:5432
address already in use
missing required environment variable DATABASE_URL
no such file or directory
If you don't understand the logs, at least find the first error. Don't just look at the last line, because the last line is often just the framework's exit message—the real cause is in the dozens of lines before it.
If you're not yet familiar with server log troubleshooting, you can also read this: How to Read VPS Logs: journalctl, systemctl, Nginx, and Application Logs.
Logs aren't always complete, especially when the process is killed by the system.
In this case, look at inspect:
docker inspect container_name --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'
Possible output:
137 true
This is very clear: the container was OOM killed.
You can also look at the full State:
docker inspect container_name --format '{{json .State}}'
If you want it formatted:
docker inspect container_name | less
Key fields:
ExitCode
OOMKilled
StartedAt
FinishedAt
Error
Health
A few common exit codes can be understood like this:
| ExitCode | Common meaning |
|---|---|
0 | Program ended normally, but the container shouldn't have ended |
1 | Application failed to start, configuration error, dependency failure |
126 | Command cannot be executed, permission or entrypoint script issue |
127 | Command doesn't exist, image or command written incorrectly |
137 | Killed by SIGKILL, common with OOM |
143 | Received SIGTERM, possibly a normal stop |
For ExitCode=1, don't directly blame Docker—look at the application logs first. For ExitCode=137, check memory first.
Many Compose files contain:
restart: always
Or:
restart: unless-stopped
This isn't a bad thing—production environments usually need it. But it can make you mistakenly think "the container keeps breaking and coming back up on its own."
First look at the restart policy:
docker inspect container_name --format '{{.HostConfig.RestartPolicy.Name}}'
If you're troubleshooting, you can temporarily stop the restart loop:
docker update --restart=no container_name
docker stop container_name
Then start it manually once:
docker start container_name
docker logs --tail=200 container_name
This makes the logs much clearer and stops the constant scrolling.
If it's a service managed by Compose, change docker-compose.yml and then:
docker compose up -d
Don't manually change too many container runtime parameters, or you'll easily end up inconsistent with the Compose file.
The most common Docker restart problem I've encountered is the first category: environment variables not passed in.
For example, the application needs:
DATABASE_URL
REDIS_URL
SECRET_KEY
JWT_SECRET
APP_URL
But .env isn't in the right place, or Compose doesn't reference it.
First look at what environment variables the container actually got:
docker inspect container_name --format '{{range .Config.Env}}{{println .}}{{end}}'
If you use Compose, confirm the .env file is in the directory where you run docker compose.
Common pitfalls:
environment:
DATABASE_URL: ${DATABASE_URL}
But the current shell or .env doesn't have DATABASE_URL at all.
You can first look at the rendered Compose configuration:
docker compose config
This command is very useful—it helps you see whether variables were ultimately substituted.
If the output still shows empty values:
DATABASE_URL: ""
Then it's no surprise the container restarts.
There are two kinds of port conflicts.
For example, Compose says:
ports:
- "80:80"
But Nginx on the host already occupies 80.
Check the port:
ss -lntp | grep ':80'
Or:
lsof -i :80
If Docker itself occupies it, you can also look at:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
Some applications listen on 3000 by default, but you think it listens on 80.
For example:
ports:
- "8080:80"
But the application actually runs on 3000 inside the container, so it should be:
ports:
- "8080:3000"
For port issues, you can continue with this: What to Do When a VPS Port Is in Use.
When the application inside the container starts, it needs to read configuration, write cache, and write logs, but if the host mount directory permissions are wrong, it will also exit directly.
For example, the logs show:
permission denied
read-only file system
cannot open config file
no such file or directory
First look at the mounts:
docker inspect container_name --format '{{json .Mounts}}'
Then look at the host directory:
ls -la /path/on/host
Common pitfalls:
- The host directory doesn't exist, and Docker automatically created an empty directory
- The file owner is
root, but the application inside the container runs as thenode,www-data, orappuser - The configuration file was mounted as a directory
- The directory was mounted to the wrong location, overwriting the original files in the image
For example, you originally wanted to mount a file:
volumes:
- ./config.yml:/app/config.yml
But the local config.yml doesn't exist, and in some cases it becomes a directory, so the application naturally can't read it.
If you encounter Docker permission issues, you can read this: What to Do About Docker Permission Denied on a VPS.
Many applications need to connect to a database or Redis at startup.
You think Compose already has:
depends_on:
- db
And everything is fine. Actually it isn't.
depends_on by default only guarantees startup order, not that the database is ready.
So the application may connect to the database as soon as it starts:
connect ECONNREFUSED db:5432
Then exit directly, and Docker pulls it up again, forming a restart loop.
A more robust approach is:
- The application itself supports connection retries
- Add a healthcheck to the database
- The application startup script waits for the database to be ready
For example, PostgreSQL:
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
app:
depends_on:
db:
condition: service_healthy
If you want to use Compose in production, I recommend reading this: How to Configure Docker Compose for Production on a VPS.
Sometimes the application actually started, but the container still keeps restarting.
In this case, check whether the healthcheck and restart policy are interacting incorrectly.
First look at the health status:
docker inspect container_name --format '{{json .State.Health}}'
Or:
docker ps
If you see:
Up 30 seconds (unhealthy)
It means the main process is still alive, but the health check is failing.
Common mistakes:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
But the image doesn't have curl at all.
Or the application health check path isn't /health, but /api/health.
Another one: the application starts slowly, but the healthcheck is too impatient:
interval: 5s
timeout: 2s
retries: 3
The application hasn't come up yet, and it's already judged as failed.
You can add start_period:
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
If you access it through a reverse proxy, note that the healthcheck runs inside the container, and localhost refers to the container itself, not the host, and not the Nginx container.
If the exit code is 137, or you see in inspect:
OOMKilled: true
Check memory first.
Look at system logs:
dmesg -T | grep -i 'killed process\|out of memory\|oom'
Look at container resources:
docker stats
Check whether Compose limits it too low:
mem_limit: 256m
Many Java, Node, and Python applications consume a burst of memory at startup. You give it 256MB, which looks enough to run, but it gets killed during the startup phase.
Handling approach:
- Give the container a more reasonable memory limit
- Add swap or zram to the VPS
- Reduce application concurrency and worker count
- Check for memory leaks
- Move heavy services like databases and search services out
For low-memory VPS, you can read this: How to Optimize Low-Memory VPS with ZRAM and Swap. If OOM is already frequent, you can also read: VPS Out of Memory? OOM Problem Solutions.
When the disk is full, the container may not immediately report "disk full," but instead show as:
- Database fails to start
- Application fails to write cache
- Logs can't be written
- overlay2 abnormalities
- Container exits right after starting
First look at the disk:
df -h
Then look at inodes:
df -i
Then look at Docker usage:
docker system df
Don't just run this right away:
docker system prune -a
This command may delete images and build cache you still need. Look clearly before cleaning.
For a more robust cleanup path, you can read this: What to Do When Docker Fills Up the Disk on a VPS.
Some containers Exited (0), not because of failure, but because the command finished executing.
For example, you wrote:
command: npm run build
After the build completes, the process exits, and Docker considers the container finished. If you also wrote:
restart: always
It will keep rebuilding, exiting, and rebuilding again.
A long-running service should be:
command: npm run start
Or the image's Dockerfile should have the correct:
CMD ["node", "server.js"]
Also pay attention to foreground/background issues. The container's main process must run in the foreground. If you put the service in the background, the main process ends, and the container also ends.
Wrong example:
nginx &
Correct approach: keep the main process in the foreground.
After changing .env, docker-compose.yml, or mount paths, just docker restart is usually not enough.
A more robust way is:
docker compose up -d --force-recreate
If the image also changed:
docker compose up -d --build --force-recreate
If you only changed a configuration file mounted from the host, check whether the application supports hot reload. If not, you still need to restart the corresponding container:
docker compose restart app
But if you changed environment variables, ports, or volume mappings, you usually need to recreate the container.
I usually go in this order:
# 1. 看所有容器状态
docker ps -a
# 2. 看最近日志
docker logs --tail=200 container_name
# 3. 看退出码、OOM、错误状态
docker inspect container_name --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'
# 4. 看 restart 策略
docker inspect container_name --format '{{.HostConfig.RestartPolicy.Name}}'
# 5. 临时停止重启循环
docker update --restart=no container_name
# 6. 手动启动一次,看第一条错误
docker start container_name
docker logs --tail=200 container_name
If it's Compose:
docker compose ps -a
docker compose logs --tail=200 service_name
docker compose config
docker compose up -d --force-recreate
Then branch by error type:
| Symptom | What to check first |
|---|---|
Exited (1) | Application logs, environment variables, configuration files, dependency services |
Exited (127) | Whether command / entrypoint is written incorrectly |
Exited (137) | OOM, memory limits, system logs |
permission denied | volume permissions, user, file owner |
address already in use | Host port or container internal port conflict |
connect refused | Whether database/Redis is ready, whether network name is correct |
unhealthy | healthcheck command, path, startup time |
The container going from Restarting to Up is only the first step.
You also need to keep looking at:
docker ps
docker logs --tail=100 container_name
docker stats
Then access the service once from outside:
curl -I http://127.0.0.1:your_port
If there's Nginx in front:
curl -I https://your-domain.com
Finally, I recommend adding these:
- A reasonable
restartpolicy—don't blindly usealwaysfor all services - Add
start_periodto healthcheck - Log limits to avoid filling up the disk
- Add resource limits to key services, but don't limit them too tightly
- Regularly back up important data volumes
For example, log limits:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
For production Compose configuration, you can refer to: How to Configure Docker Compose for Production on a VPS.
If this time it's a Docker container that keeps restarting, you'll likely encounter these problems later:
- How to Configure Docker Compose for Production on a VPS
- What to Do When Docker Fills Up the Disk on a VPS
- What to Do About Docker Permission Denied on a VPS
- What to Do When a Docker Container Can't Access the Network on a VPS
One final reminder: when you encounter Restarting, don't rush to delete the container or delete the volume. First look at the logs, exit codes, and OOMKilled. Many times what you're missing isn't a reinstall, but finding the first real error.