Skip to content

Deploying with Docker Compose#

If you have a docker-compose.yml you are in luck: you have already described how your application runs. Three things are left - install Docker on the server, make the service persistent, and expose it safely.

The shortcut

If you have connected VeriTeknik MCP to your AI assistant (Claude, ChatGPT, Cursor...), you can ask for all three at once:

"Install Docker on my server, clone the github.com/user/project repository, bring it up with docker compose and put example.com in front of it with HTTPS."

Your assistant runs the steps below on the server for you. Do not paste an .env file that holds passwords or API keys into the chat - create it on the server yourself. The permissions to turn on are in the deployment guide.


1. Install Docker#

The Docker package in distribution repositories is usually old; use the official script.

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER

Log out and back in for the group change to take effect, then verify:

docker --version
docker compose version
docker run --rm hello-world

With Morpheus

"install Docker and Docker Compose on my server"

docker compose, not docker-compose

The hyphenated form is the old v1 and is no longer supported. The modern one has a space: docker compose up. Do not be confused when older guides show the hyphen.


2. Get the project onto the server#

sudo mkdir -p /opt/myapp
sudo chown -R $USER:$USER /opt/myapp
cd /opt/myapp
git clone https://github.com/user/repo.git .

nano /opt/myapp/.env
chmod 600 /opt/myapp/.env

3. Bind ports to localhost#

This is the most important step on this page and the one most often skipped.

If your compose file says:

ports:
  - "8080:8080"

the container listens on every network interface and anyone who reaches your server's IP hits it directly. Having a firewall usually does not help, because Docker writes its own iptables rules and bypasses ufw.

Docker bypasses ufw

This is a rule we learned from a real incident: a port that looks closed in ufw may have been opened by Docker and be reachable from the internet. For an attacker looking for a foothold, that is enough.

Fix: bind the port to the loopback interface only.

ports:
  - "127.0.0.1:8080:8080"

Now only the server itself (that is, Nginx) can reach the container. Nginx becomes the single door to the outside.

Verify:

sudo ss -tlnp | grep 8080

You should see 127.0.0.1:8080, not 0.0.0.0:8080.


4. Bring it up#

cd /opt/myapp
docker compose up -d
docker compose ps
docker compose logs -f --tail=100

-d runs it in the background. You can stop following logs with Ctrl+C; the containers keep running.


5. Survive a reboot#

Add this to every service in your compose file:

restart: unless-stopped

That covers both crashes and reboots. Make sure Docker itself starts at boot:

sudo systemctl enable docker

Then actually test it - reboot and check:

sudo reboot
# once you can connect again:
docker compose -f /opt/myapp/docker-compose.yml ps

6. Expose it with Nginx#

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d example.com

See steps 4 and 5 of the Next.js page for detail; the logic is the same.


Updates#

cd /opt/myapp
git pull
docker compose pull
docker compose up -d
docker image prune -f

That last line matters: old images fill the disk silently. What happens when the disk fills up is covered in troubleshooting.

Teach Morpheus this as a habit

"from now on when I say 'update the app', run git pull, docker compose pull, docker compose up -d in /opt/myapp and clean up unused images"


Checklist#

  • [ ] docker compose ps → all services Up
  • [ ] sudo ss -tlnp → app ports bound to 127.0.0.1 only
  • [ ] Every service has restart: unless-stopped
  • [ ] sudo systemctl is-enabled docker → enabled
  • [ ] The site comes back on its own after a reboot
  • [ ] https://example.com opens with the padlock
  • [ ] docker system df shows a reasonable amount of accumulated junk