Deploy an app
Deploy your code from a folder, a git repository or a Docker image, and control how Ferry builds it.
A service is one app that Ferry builds and runs. Its code comes from a folder on your machine, a git repository or a Docker image. Each deploy gets the code, builds an image and starts the app.
Deploy from a folder
Go to the folder of your app. Then run ferry up:
cd node-hello
ferry up --followferry up packs the folder and sends it to the server. If the service does not exist, the command creates a web service with the name of the folder. Then Ferry builds the code and deploys it. --follow shows the log until the deploy ends.
Packing /home/you/ferry/examples/node-hello …
Packed 2 file(s), 576 B
Created web service 'node-hello' (srv-8593a88388f349feade2)
Uploading 576 B …
Deploy dep-3493c505ea9b4415b359 queued (upload), streaming logs…
09:36:21 ==> Detected Node.js runtime
09:36:21 ==> Generated Dockerfile.ferry for the Node.js runtime
09:36:21 ==> Build successful 🎉
09:36:23 ==> Your service is live 🎉
Deploy dep-3493c505ea9b4415b359 is live at http://node-hello.localhost:8080To deploy a new version, run ferry up again. To choose the name, write it after up: ferry up my-app. To send a different folder, add --dir ./app.
In the dashboard, the Upload later source creates a service with no code. Then run ferry up with the name of that service.
What goes into the upload
| Files | In the upload |
|---|---|
Files that .gitignore, .ignore or .ferryignore exclude | No |
.git, node_modules, target, .venv, venv, __pycache__ | No |
Hidden files, such as .env.example or .npmrc | Yes |
| Symlinks that point outside the folder | No, with a warning |
The archive can be 512 MiB at most. To leave out large files, add them to a .ferryignore file. This file uses the same rules as .gitignore.
Deploy from a git repository
ferry create api --repo https://github.com/you/api --branch main --follow--repo is the URL of the repository. --branch is the branch to deploy. The default is main.
Ferry clones the repository and builds the newest commit of the branch. To build one exact commit, give its SHA:
ferry deploy api --commit <sha>Auto-deploy is on by default. If GitHub tells Ferry about each push, a push to the branch deploys the service. See Auto-deploy from GitHub.
Ferry accepts https://, ssh:// and git:// URLs, the form [email protected]:you/api.git and the path of a repository on the server. It does not fetch git submodules.
Deploy a private repository
Ferry needs read access to clone a private repository. Connect your GitHub or GitLab account to the server one time. Then Ferry clones the repositories of that account for each service. The URL of the repository needs no password.
To connect a GitHub account:
- In the dashboard, open Server → Connections. Click Connect GitHub.
- Click Continue to GitHub. GitHub shows a new GitHub App for your server. Click Create GitHub App.
- Choose All repositories or Only select repositories. Click Install.
GitHub sends you back to Ferry, and the account shows as connected. The app asks for read access only.
To connect a GitLab account, click Connect GitLab. The dialog tells you how to create an OAuth application on GitLab, with its redirect URI and the scopes read_api and read_repository. Paste the Application ID and the Secret of the application. Click Continue to GitLab. On GitLab, click Authorize.
At each deploy, Ferry reads the URL of the repository. An https:// URL on the host of a connected account uses that account. An SSH URL, a path on the server or a URL with its own credentials uses no account.
A GitHub App reads only the account where you installed it. For an organization, connect the organization too. In the dialog, open Organization, self-hosted GitHub. Type the login of the organization. You must be an owner of the organization.
In the dialog, click Use an access token instead. A GitHub classic token needs the repo scope. A fine-grained GitHub token needs read access to Contents and Metadata. A GitLab token needs read_api and read_repository. When a token expires, the deploys of private repositories fail until you click Replace token.
You can put a token in an HTTPS URL: https://USERNAME:[email protected]/you/api. Ferry never shows the token in logs or errors. For an SSH URL, the user that runs ferryd needs an SSH key that the git host accepts, for example a deploy key.
Deploy a Docker image
ferry create hello --image nginx:alpine --port 80Ferry pulls the image from its registry and runs it. Ferry builds nothing. --port is the port where the app of the image listens. In the dashboard, choose the Docker image source. Type the name of the image in Image.
| Tag of the image | Ferry pulls it |
|---|---|
No tag, or latest | At each deploy |
Each other tag, for example 1.4 | Only when the server does not have it |
Each deploy keeps the exact image that it pulled: the pinned image. A restart or a rollback runs that image, even after the tag moves in the registry.
Ferry pulls public images only. It has no login for a private registry. If you set a start command, it replaces the command of the image.
How Ferry builds your code
A build makes a Docker image from your code. Ferry looks at the files at the root of your code to choose how.
With your Dockerfile
If your code has a Dockerfile, Ferry builds it as it is. The log shows ==> Using Dockerfile at ./Dockerfile. For a Dockerfile in a different place, set --dockerfile docker/Dockerfile.
With no Dockerfile
Ferry finds the runtime from the first file of this list. Then it writes a Dockerfile for you, Dockerfile.ferry, and builds it.
| File | Runtime |
|---|---|
package.json, bun.lock or bun.lockb | node |
requirements.txt, pyproject.toml or Pipfile | python |
go.mod | go |
Cargo.toml | rust |
Gemfile | ruby |
index.html | static |
To choose the runtime yourself, set --runtime, for example --runtime node. To change the generated build, commit your own Dockerfile.
The start command
Ferry uses the first start command that it finds:
- The start command of the service:
--start-cmd. - A line of a Procfile at the root:
web:for a web service,worker:for a worker. - The default of the runtime, for example
npm start.
Ferry runs the command with /bin/sh -c. Thus $PORT and the other variables expand. The log shows the choice:
==> Start command: npm start (the "start" script of package.json)The port
Listen on 0.0.0.0 and on PORT
Your app must listen on the address 0.0.0.0 and on the port in the PORT variable. An app that listens on 127.0.0.1 gets no traffic.
If you set --port or a PORT variable, Ferry uses that port. If not, an image that exposes one port only gives that port. In all other cases, PORT is 10000.
Variables and secrets in the build
The build gets the environment variables of the service as build secrets. A secret does not stay in the image. In your own Dockerfile, mount each variable into the step that needs it:
RUN --mount=type=secret,id=NPM_TOKEN,env=NPM_TOKEN npm ciThis needs Docker Engine 27.3 or newer on the server. The build does not get PORT and the other variables that Ferry adds when the app runs. See Variables and secrets.
The build cache
Docker keeps the result of each build step. A step that did not change does not run again. To build with no cache:
ferry deploy api --clear-cacheIn the dashboard, open the menu next to Manual deploy. Choose Clear build cache & deploy.
Settings you can change
ferry update api --start-cmd "node server.js" --health /healthzA change applies at the next deploy. In the dashboard, open the Settings page of the service. For the instances, the memory and the CPU, see Scaling and limits.
| Setting | Flag | Default |
|---|---|---|
| Branch | --branch | main |
| Runtime | --runtime | auto |
| Root directory | --root-dir | the root of the repository |
| Dockerfile path | --dockerfile | Dockerfile |
| Build command | --build-cmd | the default of the runtime |
| Start command | --start-cmd | the default of the runtime, or the command of the image |
| Publish directory (static sites) | --publish-dir | detected |
| Port | --port | detected, else 10000 |
| Health check path | --health | none: a TCP check |
| Auto-deploy | --auto-deploy, --no-auto-deploy | on |
To clear a text setting, give an empty value: --start-cmd "". With --port 0, Ferry detects the port again.
A monorepo has more than one app in one repository. For each app, set its root directory, for example --root-dir services/api. Ferry builds that folder. It looks for the files of the runtime, the Dockerfile and the Procfile in that folder.
To change the source, set the new source and clear the old source in the same command:
ferry update api --image ghcr.io/you/api:1.4 --repo ""Service types
--type sets the service type when you create the service. You cannot change it later.
| Type | --type | For |
|---|---|---|
| Web service | web (default) | A public HTTP app, with a URL |
| private service | pserv | An app that only other services call |
| Background background worker | worker | A process with no port, for example a queue consumer |
| cron job | cron | A command that runs on a schedule (--schedule) |
| static site | static | Files that nginx serves |
Delete a service
ferry delete apiThe command asks you to confirm. Then Ferry removes the containers, the images, the disk, the logs, the settings and the variables of the service. You cannot undo it.
If another service has a reference to it, Ferry refuses. --force deletes it, but the next deploy or restart of the other service fails.
Language notes
Each language works with no Dockerfile. This table shows what Ferry does, and what you set when the default does not fit.
| Language | Ferry does | You set |
|---|---|---|
| Node.js | Installs with npm, Yarn, pnpm or Bun, from the lockfile. Runs the build script, then npm start. The version comes from engines.node, else 22. | A start script, or --start-cmd. |
| Python | Installs from requirements.txt, pyproject.toml or Pipfile. Starts python main.py or python app.py. The version comes from .python-version or runtime.txt, else 3.12. | For Gunicorn, Uvicorn or Django: a Procfile line, for example web: gunicorn app:app --bind 0.0.0.0:$PORT. |
| Go | Builds the main package with the version of go.mod, then runs the binary. | For more than one command below cmd/: --build-cmd "go build -o /out/api ./cmd/api" and --start-cmd api. |
| Rust | Runs cargo build --release, then runs the binary. | For a workspace with no root package: --start-cmd with the name of the binary. |
| Ruby | Runs bundle install. With a config.ru, starts bundle exec rackup. The version comes from .ruby-version, else 3.3. | For Rails: web: bundle exec rails server -b 0.0.0.0 -p $PORT in a Procfile. |
| Static site | With a build script or --build-cmd, runs the build first. nginx serves the files on port 80. | --type static. For a folder that is not dist, build, out, public, _site or site: --publish-dir. |
Common build problems
| What you see | What to do |
|---|---|
could not detect the runtime | Add a Dockerfile or a file of the runtime table. Or set --runtime. Make sure that --root-dir is correct. |
cannot determine how to start this Node.js app | Set --start-cmd, add a start script, or add a Procfile with a web: line. |
exited with code 0 after the build | The start command ends and starts no server. Read the ==> Start command line of the log. Set the correct command. |
health check timed out | The app must listen on 0.0.0.0 and on $PORT. |
Authentication failed or not found when Ferry clones | Read the ==> Hint line. Connect the account of the repository, or add the repository to the GitHub App. |
branch 'main' not found | Push the branch, or set the correct branch with --branch. |
| A static build does not find its tools | The build command runs with NODE_ENV=production. If it installs the dependencies, use npm ci --include=dev. |
secret env mounts need Docker Engine 27.3 or newer | Upgrade Docker on the server. |
| The upload is over the upload limit | Add large files and folders to .ferryignore. |
For more problems and their fixes, see Troubleshooting.