This guide walks through the full deployment lifecycle with Bullet.
Bullet deploys applications as tarballs. Package your application files into a .tar.gz archive:
tar czf app.tar.gz src/ package.json server.jsA Makefile target is a common pattern:
.PHONY: release
release:
tar czf app.tar.gz *.js package.jsonCreate a Bulletspec file in your project root:
application:
name: My App
identifier: myapp
programs:
web:
name: Web Server
command: node server.js
container:
image: node:20-alpine
ports:
- 80:3000See the Bulletspec Reference for all available options.
Before deploying, prepare your servers:
bullet -H 192.168.0.3 setupThis installs Docker and creates the directory structure under /opt/myapp/.
To also push an environment file during setup:
bullet -H 192.168.0.3 setup --environ env.productiontar czf app.tar.gz <your files>
bullet -H 192.168.0.3 deploy app.tar.gzBullet uploads the tarball, extracts it, builds Docker images, and reloads running containers. If the release has already been deployed (same SHA256 hash), the node is skipped.
You can combine setup, environment push, and scaling into a single deploy:
bullet -H 192.168.0.3 deploy app.tar.gz --setup --environ env.production --scaleAfter the first deploy, start your containers:
bullet -H 192.168.0.3 scale web=2Or use expression-based scaling rules defined in your Bulletspec:
programs:
web:
scales:
- if: 'hasTags("production")'
n: 'hw.cores'
- n: '1'Then run scale without arguments to apply the rules:
bullet -H 192.168.0.3 scalePush environment files to your servers:
bullet -H 192.168.0.3 environ:push env.productionThe file is stored at /opt/<identifier>/env and is automatically loaded by all containers and cron jobs.
If the file has changed, running containers are restarted to pick up the new environment. This also applies to deploy --environ and setup --environ, even when the release itself is unchanged. Use environ:push --no-restart to upload a change without applying it yet.
For applications that need a custom image, specify a Dockerfile:
programs:
web:
container:
dockerfile: DockerfileBullet builds the image on the server during deploy. It tracks the Dockerfile's hash and only rebuilds when the Dockerfile changes.
By default, containers are restarted on deploy. You can configure a lighter reload:
programs:
web:
reload:
method: signal
signal: SIGHUPOr run a command inside the container:
programs:
web:
reload:
method: command
command: nginx -s reload
pre_command: nginx -tThe pre_command runs before the reload action, useful for configuration validation. If the Docker image was rebuilt during the deploy, the container is always restarted regardless of the reload method.
After setup and deployment, the server has:
/opt/<identifier>/
releases/
1711612800-abc123/ # Release directories (timestamp-hash)
1711699200-def456/
current -> releases/1711699200-def456/ # Symlink to latest (or copy)
current.hash # SHA256 of current release
env # Environment file
env.hash # SHA256 of environment file applied to containers
Containers are named <identifier>_<program>_<instance>, for example myapp_web_1, myapp_web_2.
Each container gets the following environment variables set by Bullet:
| Variable | Value |
|---|---|
BULLET_APPLICATION_NAME |
Application name from spec |
BULLET_APPLICATION_ID |
Application identifier |
BULLET_PROGRAM_KEY |
Program key (e.g. web) |
BULLET_PROGRAM_NAME |
Program display name |
BULLET_INSTANCE_ID |
Container name |
Plus all variables from the environment file.
Containers use the json-file log driver with a 3 GB max size and are configured with --restart always.
Port mappings increment the host port per instance. If the spec says 80:3000:
| Instance | Host Port | Container Port |
|---|---|---|
| 1 | 80 | 3000 |
| 2 | 81 | 3000 |
| 3 | 82 | 3000 |
Target multiple hosts at once:
bullet -H 192.168.0.3,192.168.0.4,192.168.0.5 deploy app.tar.gzOr use a node manifest with tag filtering for larger setups:
# Deploy to all production web servers
bullet -c production deploy app.tar.gzWith Bulletcfg.production:
hosts: "@nodes.yaml:production+web"See Configuration for details on node manifests and tag filtering.
Define scheduled tasks in your Bulletspec:
application:
cron:
jobs:
- key: cleanup
command: ./cleanup
schedule: daily
jitter: 1h
healthcheck:
url: https://hc-ping.com/abc123Enable them on your servers:
bullet -H 192.168.0.3 cron:enable cleanupCron jobs run as one-off Docker containers managed by systemd timers. They use the same environment file and application directory as regular programs.
Check their status:
bullet -H 192.168.0.3 cron:statusDisable when no longer needed:
bullet -H 192.168.0.3 cron:disable cleanupSome deploys need a one-off command afterwards, such as building database indexes or running migrations. Define these as post-deploy hooks:
application:
hooks:
post_deploy:
- key: makeindexes
command: ./clt db:makeindexesAfter every node is deployed, Bullet runs each hook once, on the first selected node, in a one-off container using the shell program's image. Set nodes: all to run a hook on every node, and program to use a different program's image. See Hooks for all fields.
Skip hooks for a deploy with --skip-hooks, or run a hook manually:
bullet -H 192.168.0.3 hook:run makeindexesCheck container status across nodes:
bullet statusTail logs:
bullet -H 192.168.0.3 log web
bullet -H 192.168.0.3 log web:2 # Instance 2Forward a port for local debugging:
bullet -H 192.168.0.3 forward 3000:8080Access the server directly:
bullet -H 192.168.0.3 host:shell
bullet -H 192.168.0.3 host:df
bullet -H 192.168.0.3 host:topRemove old releases to free disk space:
bullet -H 192.168.0.3 pruneThis keeps the 5 most recent releases. Pruning also happens automatically after each deploy.