Conteúdo
Tutorial de implantação com Docker: publique um site em um servidor na nuvem (VPS)
Nesta seção, vamos reunir o que você aprendeu até aqui: implantar em um servidor Linux na nuvem (VPS) um site gerenciado com Docker Compose.
As saídas desta seção simulam o fluxo completo em uma única máquina: usamos o
registry:3local no lugar do Docker Hub ou do GitHub Container Registry (localhost:5000) e um certificado local emitido pelo Caddy paralocalhostno lugar do certificado de um domínio real. Em uma implantação real, basta substituir os nomes das imagens e o domínio pelos seus.
Fluxo geral
Local ou CI ──docker build / push──▶ Registry ──docker compose pull──▶ Servidor na nuvem
O código é transformado em imagem por um build local ou no CI e enviado ao Registry; o servidor só baixa a imagem e a executa, sem fazer o build no servidor. Assim, o servidor não precisa do código-fonte nem das ferramentas de build, e o resultado de cada implantação é idêntico. Para voltar a uma versão anterior, basta trocar pela tag antiga.
1. Preparar o servidor
Pode ser um servidor Linux de qualquer provedor de nuvem. Vamos usar o Ubuntu como exemplo:
-
Conecte-se via SSH, crie um usuário comum e desative o login por senha (use somente chaves).
-
Siga as instruções para Linux em Instalar Docker e instale o Docker Engine e o plugin do Compose.
-
No firewall, libere somente as portas 22 (SSH), 80 e 443:
sudo ufw allow OpenSSH sudo ufw allow 80,443/tcp sudo ufw enableAtenção: as portas abertas com
-pignoram o ufw (veja Boas práticas de segurança); por isso, no compose.yaml abaixo, somente o proxy reverso temports. -
Configure a inicialização automática do Docker:
sudo systemctl enable docker.
2. Aplicação
O exemplo é um site pequeno com Node.js + PostgreSQL. A cada solicitação, ele registra uma visita e retorna a versão atual e o número de visitas.
myapp/
├── package.json
├── package-lock.json
├── server.js
├── Dockerfile
└── .dockerignore
server.js:
const express = require("express");
const { Pool } = require("pg");
const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const version = process.env.APP_VERSION || "dev";
app.get("/", async (req, res) => {
await pool.query("CREATE TABLE IF NOT EXISTS visits (id serial PRIMARY KEY, at timestamptz DEFAULT now())");
await pool.query("INSERT INTO visits DEFAULT VALUES");
const { rows } = await pool.query("SELECT count(*)::int AS visits FROM visits");
res.json({ app: "myapp", version, visits: rows[0].visits });
});
const server = app.listen(3000, () => console.log(`myapp ${version} listening on port 3000`));
process.on("SIGTERM", () => server.close(() => pool.end().then(() => process.exit(0))));
As dependências de package.json são express e pg. O Dockerfile (veja a explicação sobre a configuração em Aplicação Node.js) recebe o número da versão por meio de ARG e o grava em ENV para que o programa possa exibir sua própria versão:
FROM node:24-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --chown=node:node . .
ARG APP_VERSION=dev
ENV APP_VERSION=$APP_VERSION
USER node
EXPOSE 3000
CMD ["node", "server.js"]
3. Construa e envie a imagem
Na sua máquina ou no CI, use um número de versão fixo como tag:
$ docker build --build-arg APP_VERSION=1.0.0 -t localhost:5000/myapp:1.0.0 .
[+] Building 6.7s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 308B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 63B 0.0s
=> [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1 2.4s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1 0.0s
=> => extracting sha256:e2de96513ba9eb53b431787ec8a65cdde380ac4772a3e4c4b714dcfde2a102b5 0.1s
=> => extracting sha256:3c504d5a797be28f9de43147d49cd8ecb2eea46dfaf7beb8715db695d5f0a7f0 2.1s
=> => extracting sha256:02f40d53b06f6b91fec0cd6a6719656da99270bff8ee54ed8dcdf4c157763fc1 0.1s
=> => extracting sha256:7ce7dcbafb7f750cf0c6be3c30e34372b06dd1182fa67f6ae30782c8150c5444 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 36.48kB 0.0s
=> [2/5] WORKDIR /app 0.1s
=> [3/5] COPY package.json package-lock.json ./ 0.0s
=> [4/5] RUN npm ci --omit=dev && npm cache clean --force 3.5s
=> [5/5] COPY --chown=node:node . . 0.1s
=> exporting to image 0.6s
=> => exporting layers 0.3s
=> => exporting manifest sha256:a0bd86a3b62021adb05a5e710c5a59f9e6287c8c84016ba22f1923bc4aaeeabf 0.0s
=> => exporting config sha256:7f75124e75541f256217e31a844e8c9b687434955d053e47e5dae571aa3944c6 0.0s
=> => exporting attestation manifest sha256:9d6688e6e287f9a0b3d3a95d23765cde8496c89bf6dd0c7fd7a191ac58abe9db 0.0s
=> => exporting manifest list sha256:d7d0950bb72768da05a47e8bf234754cecde01a8fd8d920e8261fcf7bf031522 0.0s
=> => naming to localhost:5000/myapp:1.0.0 0.0s
=> => unpacking to localhost:5000/myapp:1.0.0 0.3s
$ docker push localhost:5000/myapp:1.0.0
The push refers to repository [localhost:5000/myapp]
44136fa355b3: Pushed
2297ea3ee753: Pushed
7ce7dcbafb7f: Pushed
fffb74b09a22: Pushed
e2de96513ba9: Pushed
1d73dbdb2af1: Pushed
bd9735416a50: Pushed
02f40d53b06f: Pushed
22ac32835411: Pushed
3c504d5a797b: Pushed
1.0.0: digest: sha256:d7d0950bb72768da05a47e8bf234754cecde01a8fd8d920e8261fcf7bf031522 size: 856
Na prática, o nome da imagem é o endereço do Registry seguido da sua conta, por exemplo, yourname/myapp:1.0.0 no Docker Hub ou ghcr.io/yourname/myapp:1.0.0 no GitHub. Antes disso, execute docker login (consulte Envie para o Docker Hub).
Ao construir em um Mac com Apple Silicon e implantar em um host amd64 comum, lembre-se de adicionar
--platform linux/amd64. Caso contrário, o servidor exibiráexec format error. Consulte Imagens multiplataforma.
4. Arquivos no servidor
No servidor, crie uma pasta, por exemplo /srv/myapp. Você só precisa de três arquivos, não precisa do código-fonte:
/srv/myapp/
├── compose.yaml
├── .env
└── Caddyfile
compose.yaml:
services:
caddy:
image: caddy:2
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- app
restart: unless-stopped
logging:
options: { max-size: "10m", max-file: "3" }
app:
image: localhost:5000/myapp:${APP_VERSION}
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
depends_on:
db:
condition: service_healthy
restart: unless-stopped
logging:
options: { max-size: "10m", max-file: "3" }
db:
image: postgres:18
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U app -d app"]
interval: 5s
retries: 10
restart: unless-stopped
logging:
options: { max-size: "10m", max-file: "3" }
volumes:
caddy_data:
caddy_config:
pgdata:
.env:
APP_VERSION=1.0.0
DB_PASSWORD=change-this-to-a-long-random-password
Caddyfile (na implantação real, substitua localhost pelo seu domínio, por exemplo example.com):
localhost {
reverse_proxy app:3000
}
Pontos principais:
| Configuração | Descrição |
|---|---|
image: ...:${APP_VERSION} | A versão é controlada por .env; para atualizar ou reverter, basta alterar esta linha |
Somente caddy tem ports | Não é possível acessar o aplicativo nem o banco de dados externamente |
| Caddy | Solicita e renova certificados HTTPS automaticamente e redireciona HTTP para HTTPS. Consulte a seção final de Nginx e proxy reverso |
pgdata:/var/lib/postgresql | Caminho de montagem do PostgreSQL 18. Consulte PostgreSQL |
pg_isready -h 127.0.0.1 | Evita que o banco de dados seja considerado healthy durante a inicialização |
restart: unless-stopped | Inicia automaticamente após uma falha do aplicativo ou a reinicialização do host |
logging | Limita o tamanho dos logs para evitar que encham o disco. Consulte Liberar espaço em disco |
O arquivo .env contém uma senha. Defina as permissões: chmod 600 .env.
5. Inicie
$ ls -a
.
..
.env
Caddyfile
compose.yaml
$ docker compose pull
[+] pull 35/35
✔ Image caddy:2 Pulled 2.8s
✔ Image localhost:5000/myapp:1.0.0 Pulled 0.3s
✔ Image postgres:18 Pulled 12.2s
$ docker compose up -d
[+] up 7/7
✔ Network srv-myapp_default Created 0.0s
✔ Volume srv-myapp_caddy_config Created 0.0s
✔ Volume srv-myapp_caddy_data Created 0.0s
✔ Volume srv-myapp_pgdata Created 0.0s
✔ Container srv-myapp-db-1 Healthy 6.0s
✔ Container srv-myapp-app-1 Started 6.2s
✔ Container srv-myapp-caddy-1 Started 6.7s
$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
srv-myapp-app-1 localhost:5000/myapp:1.0.0 "docker-entrypoint.s…" app 10 seconds ago Up 4 seconds 3000/tcp
srv-myapp-caddy-1 caddy:2 "caddy run --config …" caddy 10 seconds ago Up 3 seconds 0.0.0.0:80->80/tcp, [::]:80->80/tcp, 0.0.0.0:443->443/tcp, [::]:443->443/tcp, 443/udp, 2019/tcp
srv-myapp-db-1 postgres:18 "docker-entrypoint.s…" db 10 seconds ago Up 9 seconds (healthy) 5432/tcp
$ curl -sk https://localhost/
{"app":"myapp","version":"1.0.0","visits":1}
$ curl -sk https://localhost/
{"app":"myapp","version":"1.0.0","visits":2}
$ docker compose logs app
app-1 | myapp 1.0.0 listening on port 3000
docker compose pullbaixa primeiro todas as imagens, incluindomyapp:1.0.0, que enviamos.up -dinicia os serviços na ordem das dependências:appsó inicia depois quedbficaHealthy; por último, iniciacaddy.- Em
ps, somente caddy expõe as portas 80 e 443 externamente. - Faça duas solicitações por HTTPS.
visitspassa de 1 para 2, e os dados são gravados no PostgreSQL. (-ké usado porque aqui estamos usando um certificado local; em um domínio de produção, o certificado do Let's Encrypt dispensa-k.)
Na implantação em produção, acesse https://seu-dominio para confirmar que o site está funcionando e use docker compose logs -f app para acompanhar os logs.
6. Atualizar a versão
Construa a imagem localmente e envie a nova versão:
$ docker build -q --build-arg APP_VERSION=1.1.0 -t localhost:5000/myapp:1.1.0 .
sha256:aac108fcd63549926e767b781e0b9aa25a86e2f2cd449710a41b5e8d0accda16
$ docker push -q localhost:5000/myapp:1.1.0
localhost:5000/myapp:1.1.0
No servidor, altere APP_VERSION em .env e, em seguida, baixe e recrie somente app:
$ grep APP_VERSION .env
APP_VERSION=1.1.0
$ docker compose pull app
[+] pull 1/1
✔ Image localhost:5000/myapp:1.1.0 Pulled 0.1s
$ docker compose up -d app
[+] up 2/2
✔ Container srv-myapp-db-1 Healthy 0.7s
✔ Container srv-myapp-app-1 Started 1.0s
$ curl -sk https://localhost/
{"app":"myapp","version":"1.1.0","visits":3}
$ docker compose images app
CONTAINER REPOSITORY TAG PLATFORM IMAGE ID SIZE CREATED
srv-myapp-app-1 localhost:5000/myapp 1.1.0 linux/amd64 aac108fcd635 251MB 44 seconds ago
pull appbaixa somente a nova imagem deapp. (Como estamos simulando tudo na mesma máquina, a imagem já está localmente, então levou apenas 0,1 segundo.)up -d appdetecta que a imagem mudou e recria somente o contêinerapp;dbecaddynão são afetados, e o site fica fora do ar por cerca de 1 segundo.- A resposta mostra
versioncomo1.1.0;visitscontinua aumentando a partir de 3, e os dados do banco de dados são preservados. docker compose images appconfirma que a versão1.1.0está em uso.
Reverter a versão
Se a nova versão apresentar problemas, altere APP_VERSION de volta para 1.0.0 e execute up -d app:
$ grep APP_VERSION .env
APP_VERSION=1.0.0
$ docker compose up -d app
[+] up 2/2
✔ Container srv-myapp-db-1 Healthy 0.7s
✔ Container srv-myapp-app-1 Started 0.9s
$ curl -sk https://localhost/
{"app":"myapp","version":"1.0.0","visits":4}
Um único comando restaura a versão anterior, e a imagem antiga ainda está no servidor, então não é preciso baixá-la novamente. Por isso você deve usar números de versão fixos em vez de latest: com latest, você não sabe ao certo qual versão está em execução no servidor e não consegue voltar explicitamente para a versão anterior.
Alterações na estrutura do banco de dados (migration) geralmente não podem ser revertidas automaticamente. Faça backup do banco de dados antes de colocar uma nova versão em produção, e prefira que as alterações no banco de dados da nova versão sejam compatíveis com a versão anterior do programa.
7. Backup
No mínimo, faça backup do banco de dados todos os dias e salve o arquivo em outra máquina. Crie backup.sh em /srv/myapp:
#!/bin/sh
set -e
cd "$(dirname "$0")"
mkdir -p backup
docker compose exec -T db pg_dump -U app app | gzip > backup/app-$(date +%F).sql.gz
find backup -name 'app-*.sql.gz' -mtime +14 -delete
$ ./backup.sh && ls -lh backup
total 4K
-rw-r--r-- 1 root root 872 Oct 13 01:17 app-2026-10-13.sql.gz
$ gunzip -c backup/app-*.sql.gz | grep -A 6 "^COPY public.visits"
COPY public.visits (id, at) FROM stdin;
1 2026-10-13 01:16:44.277875+00
2 2026-10-13 01:16:44.390504+00
3 2026-10-13 01:16:57.763022+00
4 2026-10-13 01:17:02.44854+00
\.
- O arquivo de backup recebe a data no nome, e
find ... -mtime +14 -deleteremove os backups com mais de 14 dias. - Confira o conteúdo após descompactar: os 4 registros da tabela
visitsestão lá.
Configure chmod +x backup.sh e use crontab -e para executá-lo todos os dias às 3h da manhã:
0 3 * * * /srv/myapp/backup.sh
Se o backup ficar na mesma máquina que o servidor, ele será perdido junto com o servidor em caso de falha. Portanto, use ferramentas como rsync ou rclone para copiar backup/ para outra máquina ou para um armazenamento na nuvem. Consulte Backup e restauração do PostgreSQL para ver como restaurar e teste a restauração periodicamente.
8. Automação (opcional)
Atualizar entrando manualmente por SSH logo se torna trabalhoso. Algumas formas comuns de automatizar:
-
CI/CD: construa e envie a imagem em um CI, como GitHub Actions, e depois use SSH para alterar
APP_VERSIONno servidor e executardocker compose pull app && docker compose up -d app. -
Docker context: opere o Docker do servidor remoto diretamente da sua máquina, sem precisar entrar primeiro no servidor:
docker context create prod --docker "host=ssh://user@your-server" docker --context prod psdocker --context prod compose up -dusa ocompose.yamldo diretório local e executa o comando no servidor remoto.
Lista de verificação antes de colocar em produção
- O SSH permite apenas login com chave, e o firewall libera somente as portas 22, 80 e 443
- Somente o proxy reverso tem
ports; o banco de dados não está exposto externamente - As tags das imagens usam números de versão fixos, sem
latest - Todos os serviços têm
restart: unless-stopped - O limite de tamanho dos logs está configurado
- A verificação de integridade está escrita corretamente (
pg_isready -h 127.0.0.1) - As permissões de
.envsão 600, e o arquivo não foi enviado ao Git - O banco de dados recebe backups automáticos diários em outro local, e a restauração foi testada na prática
- Imagens antigas são removidas regularmente (consulte Liberar espaço em disco), e as imagens são reconstruídas periodicamente para receber atualizações de segurança (consulte Melhores práticas de segurança)
Resumo
- Construa a imagem localmente ou no CI, envie-a para um Registry, e o servidor só precisa baixá-la e executá-la.
- Mantenha o número da versão em
.env; para atualizar ou reverter, basta alterar uma linha e executarup -d. - Somente o proxy reverso deve ficar exposto, e a verificação de integridade do banco de dados precisa estar correta.
- Faça backups automáticos diários em outra máquina e teste a restauração periodicamente.
Exercícios
Exercício 1: Depois de reverter APP_VERSION no servidor de 1.1.0 para 1.0.0, os dados gravados no banco de dados pela nova versão continuam lá?
Resposta
Sim. A reversão troca somente a imagem de app; o Volume do banco de dados não muda. Neste capítulo, vimos que visits continuou aumentando. Porém, se a nova versão tiver alterado a estrutura das tabelas, o programa antigo talvez não consiga usar a nova estrutura. Por isso, faça um backup antes de colocar a nova versão em produção.
Perguntas frequentes
Preciso usar um Registry? Não posso executar docker build no servidor?
Pode, mas o servidor precisará do código-fonte e das ferramentas de build, e cada implantação poderá gerar um resultado diferente. Com a implantação por Registry, o servidor só precisa baixar imagens que já foram testadas, e o rollback também fica simples.
Que tipo de servidor na nuvem devo usar?
Qualquer servidor Linux que permita instalar o Docker Engine serve. Para um site pequeno, normalmente 1 a 2 GB de memória bastam; aumente esse valor se o banco de dados for maior.
É possível automatizar a implantação?
Sim. Use o GitHub Actions para fazer o build e o envio ao criar uma tag de versão e, em seguida, conectar-se ao servidor via SSH para atualizá-lo. Veja [Build automático com GitHub Actions](/pt/docker/ci-cd/).
Atualizado 14 de outubro de 2026