Deploy from CI
This page shows how to deploy from a pipeline: create a token for it, keep deploy.yaml in the repository, pass the image the pipeline built, run migrations before the new version starts, and let the exit code of shipwick deploy decide whether the job passes.
Before you begin
- The agent's API must be reachable from the CI runner. The usual way is to serve it over HTTPS by giving the agent a hostname (
SHIPWICK_AGENT_DOMAIN), as described in Install Shipwick on a server. - The pipeline must have pushed the image to a registry the server can pull from. For private registries, see Pull from private registries.
- Store the agent URL and a token as secrets in your CI system. Give the pipeline a token of its own, with the
deployrole, as described next.
Create a token for the pipeline
Do not put the root token in CI. It has the admin role, which is equivalent to root SSH access to the server, and it cannot be revoked short of changing it on the agent. A pipeline needs less: the deploy role deploys, redeploys, rolls back, stops and starts, and cannot delete applications, manage tokens or touch backups. From a machine where you are logged in with an admin token:
shipwick token create ci --role deploy✓ Created token ci with the deploy role
swk_Xk3nM9…
Store it now: it will not be shown again.
In CI, set SHIPWICK_AGENT_TOKEN to it. On a machine you work from, save it with: shipwick loginThe agent keeps only the SHA-256 of the token. shipwick token ls shows when the token was last used, and shipwick token revoke ci ends it the moment a runner or a secret store is compromised; deployments the pipeline made stay in the history, marked with the token's name. See Create tokens for CI and teammates.
Connect without logging in
A CI job needs no shipwick login. Set two environment variables:
| Variable | |
|---|---|
SHIPWICK_AGENT_URL | The agent's URL, for example https://agent.example.com |
SHIPWICK_AGENT_TOKEN | The token created above |
The environment wins over any saved context, so --context and SHIPWICK_CONTEXT have no place in a pipeline: a runner has no config file, and a job that deploys to a second server sets the two variables to that server's values instead.
shipwick never accepts the token as a flag, because arguments show up in ps and in logs. If the URL is plain HTTP and not the local machine, shipwick prints a warning on standard error before it sends the token.
If the token's role does not cover a command, the agent refuses it and the job fails:
This token may not do that: it has the read role.
Use a token with the deploy role, or create one with: shipwick token create <name> --role deployDeploy the image you just built
--image overrides the image in deploy.yaml for this deployment:
shipwick deploy --image ghcr.io/company/my-api:$GIT_SHAThe override is applied to the YAML document in memory. The agent still receives one plain deploy.yaml, and the file on disk is untouched. The image's tag becomes the deployment's version, so tagging images with the commit SHA makes every entry in the history traceable to a commit.
Supply secrets with $
A value that must not be in the repository, such as a database password, is written as ${NAME} in deploy.yaml:
env:
DATABASE_URL: postgres://app:${DATABASE_PASSWORD}@postgres:5432/appshipwick deploy fills it in before the file is validated or sent. In CI, the simplest source is the environment: expose the secret as a variable named like the placeholder, DATABASE_PASSWORD here, the same way the token is exposed. Alternatively, write a NAME=value file and pass it with --env-file:
shipwick deploy --image "ghcr.io/company/my-api:$GIT_SHA" --env-file .env.productionA variable set in the environment wins over the same name in an --env-file. A placeholder that is set nowhere fails the job before anything is sent: deploy.yaml: refers to ${DATABASE_PASSWORD}, which is not set. The value is never printed; the output says only (1 variable substituted). The rules are in the CLI reference.
Run migrations before the new version starts
A pipeline that runs database migrations as a separate step needs a connection to the database from the runner, which the server does not offer, and has to get the order right by itself. pre_deploy puts the migration inside the deployment instead:
pre_deploy:
command: ["dotnet", "Migrate.dll"]
timeout: 10mThe command runs on the server, from the new image, with the application's environment and limits and on its network — it reaches postgres:5432 like a replica does — once the image is pulled and before any replica of the new version exists. shipwick deploy shows it as two steps:
✓ Pulled image ghcr.io/company/my-api:1.4.2
✓ Running pre-deploy command
✓ Pre-deploy command finished (12s)
✓ Started 1 containerIf the command exits non-zero or outlives its timeout, the deployment fails before anything was started, the job fails with it, and the last lines of the command's output are under the error:
✗ Deployment failed
pre-deploy command exited 1
Last output of the pre-deploy command:
Npgsql.PostgresException: 42P07: relation "orders" already exists
my-api is still running 1.4.1; the failed deployment did not affect it.The command runs next to the version that is still serving, so a migration must be compatible with the old code: add a column, do not drop one. That is the same backward compatibility a rolling update asks of migrations anyway. See Run scheduled jobs and one-off commands.
Deploy several applications
-f repeated deploys several applications in order, one after the other, and stops at the first failure:
shipwick deploy -f api/deploy.yaml -f worker/deploy.yaml --env-file .env.productionEvery file is validated before the first deployment starts. If one fails, the command prints Stopped at worker: 1 of 2 applications deployed. and exits with 1; the applications already deployed stay deployed. On success the last line is 2 of 2 applications deployed.
--image applies to one application and is refused with several files. Pin the image of each application in its own deploy.yaml instead, or run one deploy --image per application:
shipwick deploy -f api/deploy.yaml --image "ghcr.io/company/api:$GIT_SHA"
shipwick deploy -f worker/deploy.yaml --image "ghcr.io/company/worker:$GIT_SHA"A generic pipeline step
#!/bin/sh
set -eu
# Install shipwick. Pin the version so the pipeline does not change under you.
curl -fsSL https://get.shipwick.com | SHIPWICK_VERSION=v0.3.0 sh -s -- --cli
# SHIPWICK_AGENT_URL and SHIPWICK_AGENT_TOKEN (a deploy token) come from the
# CI system's secret store, as environment variables.
# Run from the directory that holds deploy.yaml.
shipwick deploy --image "ghcr.io/company/my-api:$GIT_SHA"The installer puts shipwick in /usr/local/bin and uses sudo if that directory is not writable. To install elsewhere, set SHIPWICK_BIN_DIR to a directory on the PATH.
A GitHub Actions job
This job assumes an earlier step or job has built and pushed ghcr.io/company/my-api:<commit SHA>, and that the repository has two secrets, SHIPWICK_AGENT_URL and SHIPWICK_AGENT_TOKEN, the latter holding the deploy token created above.
jobs:
deploy:
runs-on: ubuntu-latest
env:
SHIPWICK_AGENT_URL: ${{ secrets.SHIPWICK_AGENT_URL }}
SHIPWICK_AGENT_TOKEN: ${{ secrets.SHIPWICK_AGENT_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Install shipwick
run: curl -fsSL https://get.shipwick.com | SHIPWICK_VERSION=v0.3.0 sh -s -- --cli
- name: Deploy
run: shipwick deploy --image "ghcr.io/company/my-api:${{ github.sha }}"Exit codes
| Code | Meaning |
|---|---|
0 | Success |
1 | Anything else — including a deployment that the agent accepted but that then failed |
This makes shipwick deploy safe to use as a pipeline gate. When a deployment fails, the output says why, includes the last log lines of the replica that failed, and says whether the running version was affected:
✗ Deployment failed
replica 1 exited with code 1 shortly after start
Last output of replica 1:
panic: DATABASE_URL is not set
my-api is still running 1.4.1; the failed deployment did not affect it.A failed deployment is undone by the agent. The job fails; the application keeps serving the previous version.
Behavior in a pipeline
- Output is plain when piped. No colors and no progress line.
NO_COLORis honored too. Warnings go to standard error, so standard output stays parseable. deployreturns when the deployment is complete, not at the first sign of success. When it returns, the nextdeployfor the same application will not be refused with "operation in progress".- One deployment per application at a time. A second one is refused, not queued, and
shipwickexits 1 withAnother operation is already in progress for this application.If two pipeline runs can overlap, serialize the deploy job. - Cancelling the job does not cancel the deployment. Interrupting
shipwick deploystops the waiting; the deployment continues on the server. - Short agent outages are tolerated. While waiting,
shipwickrides out connection failures for a limited number of polls before it gives up.
Return immediately with --no-wait
shipwick deploy --image ghcr.io/company/my-api:$GIT_SHA --no-wait✓ Deployment #8 started
Follow it with: shipwick status my-apiWith --no-wait, the command returns as soon as the agent has accepted the deployment and exits 0. It says nothing about whether the deployment succeeds, so it is not a gate. Check the outcome later with shipwick status.
Deploy without deploy.yaml
redeploy deploys the configuration the agent stored with the active deployment, env values included, and optionally changes the image. It needs no deploy.yaml:
shipwick redeploy my-api --image ghcr.io/company/my-api:$GIT_SHAThe application must already have a successful deployment. Exit codes and --no-wait work as they do for deploy. See Roll back and redeploy.
Be told how it went
The pipeline's log is one place to look. With SHIPWICK_WEBHOOK_URL set on the agent, every deployment's outcome — succeeded, failed, rolled back — is also posted to a Slack or Discord channel or to an endpoint of yours, whoever started it. See Get notified.
What's next
- Create tokens for CI and teammates: roles, revocation, what each token did.
- Roll back when a version that deployed successfully turns out to be wrong.
- The shipwick reference.