-
Notifications
You must be signed in to change notification settings - Fork 106
feat: deploy command core logic and implementation #1207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: deploy command core logic and implementation #1207
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1207 +/- ##
==========================================
+ Coverage 67.67% 67.90% +0.23%
==========================================
Files 250 250
Lines 14116 14452 +336
==========================================
+ Hits 9553 9814 +261
- Misses 4181 4224 +43
- Partials 382 414 +32 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
At the moment, this does not support running the command on a windows based system since the command assumes that the environment it is running in has access to the following CLI commands:
|
|
@hwbrzzl I've completed the tests I plan on doing. I've listed the different tests that I did at the bottom of the description of this PR (above). The code coverage can't hit the target of over 68% without having a dedicated server where you can run the full deployment test through every time. This does not seem worth it to me since I already have tests that cover the way the commands are being generated. I'll go ahead and create a PR for the docs now that this is getting finalized. |
|
Thanks @cggonzal, sorry for the delay, I'll review this PR today. |
hwbrzzl
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have run the command locally, and the process is amazing. I can visit the site successfully. Left more comments, and how about adding another command to revert the deployment? To clear the app, caddy, ufw, etc. We can create an issue to trace this.
Happy to hear you like it. I'm glad it is useful to the project. Adding another command to revert the deployment sounds good to me. I have created an issue here to discuss: goravel/goravel#804 |
|
@hwbrzzl Sorry for the delay on this, I have been busier than expected lately. I've gone ahead and made the changes you requested. I believe any further optimization / usability changes can be made in a later PR. Once you review this and are okay with it, I believe we are good to merge. I have also gone ahead and opened a PR for the config changes that would need to be added to the Regarding documentation, I believe the best thing to do will be to merge in the documentation PR last so that it can be updated to include the final changes that were merged in. |
|
Thanks @cggonzal , I'll check the latest commits. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| sudo systemctl daemon-reload | ||
| sudo systemctl restart "$SERVICE" || sudo systemctl start "$SERVICE" | ||
| '`, opts.sshKeyPath, opts.sshPort, opts.sshUser, opts.sshIp, appDir, opts.appName) | ||
| return exec.Command("bash", "-lc", script) |
Copilot
AI
Oct 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rollbackCommand function uses hardcoded 'bash' shell instead of calling makeLocalCommand(script) like other commands. This breaks cross-platform compatibility on Windows where the shell should be 'cmd /C'. Replace with makeLocalCommand(script) to ensure consistent platform-specific shell selection.
| return exec.Command("bash", "-lc", script) | |
| return makeLocalCommand(script) |
hwbrzzl
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your amazing work. I think it's good to go, given it's really a big PR now. We can optimize the remaining comments in another PR.
| opts.domain = r.config.GetString("app.deploy.domain") | ||
| opts.prodEnvFilePath = r.config.GetString("app.deploy.prod_env_file_path") | ||
| opts.deployBaseDir = r.config.GetString("app.deploy.base_dir", "/var/www/") | ||
| opts.envDecryptKey = r.config.GetString("app.deploy.env_decrypt_key") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The key can be a command flag instead of a configuration. It should be secret.
console/console/deploy_command.go
Outdated
| }, | ||
| &command.BoolFlag{ | ||
| Name: "force-setup", | ||
| Aliases: []string{"f"}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Em, I mean the aliases can be removed directly.
| return nil | ||
| if opts.remoteEnvDecrypt { | ||
| remoteDecrypt = true | ||
| remoteEncName = filepath.Base(opts.prodEnvFilePath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| remoteEncName = filepath.Base(opts.prodEnvFilePath) | |
| remoteEnvName = filepath.Base(opts.prodEnvFilePath) |
| opts.staticEnv = r.config.GetBool("app.build.static") | ||
| opts.reverseProxyEnabled = r.config.GetBool("app.deploy.reverse_proxy_enabled") | ||
| opts.reverseProxyTLSEnabled = r.config.GetBool("app.deploy.reverse_proxy_tls_enabled") | ||
| opts.remoteEnvDecrypt = r.config.GetBool("app.deploy.remote_env_decrypt") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be unnecessary, the prod env can be decrypted remotely by default. Otherwise, the local env file will be covered.
* start core logic implementation * modify env example to have relevant deploy environment variables * modify environment variables and deployment options * refine server set up * comment changes * bug fixes from audit * add comment with documentation at the top of deploy command file * update comment * update tests * rearrange comment usage to bottom * more comments tweaks * clean up created files during tests * add checks for runtime environment and scp, ssh, bash, dependencies * test fixes * windows tests bug fix * increase code coverage on tests * increase code coverage * Revert "increase code coverage" This reverts commit f3a4b91. * Revert "increase code coverage on tests" This reverts commit 6ab1db4. * add missing env variable to example * properly handle rollback based on deploy tests * modify tests * address comments * remove domain nil check * use existing file existence check function * added EnvString() and EnvBool() functions * introduce windows support * increase test coverage for windows * change DEPLOY_APP_PORT to DEPLOY_REVERSE_PROXY_PORT * cahnge DEPLOY_IP_ADDRESS to DEPLOY_SSH_IP * remove deployment configuration from .env file and instead put in the config of app * make remote base directory a variable instead of forcing to /var/www * stop mocking build command and just call it directly via CLI * handle encrypted env file * change getAllOptions to getDeployOptions * change getWhichFilesToUpload() to getUploadOptions() * make setupServerCommand only take in 1 value * change parameters into commands so they are minimized * mock config and mock context initialization * add more deploy success and failure tests * put config keys inside of app.deploy.*** and app.build.*** * update tests * zip backups and .prev safety * only upload storage on initial setup * add tests for new EnvString() and EnvBool() config functions * update deploy command documentation comment * change ipAddress to sshIP and appPort to reverseProxyPort * make rollback command take opts as the only parameter * use artisan facade for build command * use content to check if env file is encrypted * modify isServerAlreadySetup() to take in opts as the only parameter * remove os check * update wording for local host validation * modify test maybe checks * resolve lint error * add tests * update tests * fix lint error * fix failing CI * sort deployOptions struct alphabetically * comment update * modify error handling in validLocalHost * remove unused param * modify getDeployOptions error handling to not exit * add tests for isEncryptedEnvContent function * support env decrypt deploys * change force-setup flag to --force * get APP_ENV from .env file instead of putting it into systemd environment * have app prefer the http.port for the reverse proxy setting * address force setup comments * allow caddy to support tls off mode
📑
Follow on PR to implement the core logic addressed in issue 778.
@hwbrzzl I'm doing some final tests, but this PR is ready to start being reviewed. Unfortunately it is a bit bigger than I expected but a lot of it is comments and tests.
Once this PR is merged, I will create another PR into the docs repo in order to document this command alongside the
compiledocumentation section that exists there.The following is documented in the code but I want to put it here as well since it serves as a guide for anyone looking through the PR.
DeployCommand
Overview
This command implements a simple, opinionated deployment pipeline for Goravel applications.
It builds the application locally, performs a one-time remote server setup, uploads the
required artifacts to the server, restarts a systemd service, and supports rollback to the
previous binary. The goal is to provide a pragmatic, single-command deploy for small-to-medium
workloads.
This command only supports Debian-based (Ubuntu, Debian, etc.) based remote servers as well as MacOS and Linux based local hosts.
Architecture assumptions
Two primary deployment topologies are supported:
Reverse proxy in front of the app (recommended)
and automatically provisions certificates; otherwise Caddy serves plain HTTP on :80
No reverse proxy
Artifacts & layout on server
Remote base directory: /var/www/<APP_NAME>
Files managed by this command on the remote host:
main: current binary (running)main.prev: previous binary (standby for rollback).env: environment file (uploaded from DEPLOY_PROD_ENV_FILE_PATH)public/: optional static assetsstorage/: optional storage directoryresources/: optional resources directoryIdempotency & first-time setup
The initial server setup is performed exactly once per server (per app name). The command first
checks if /etc/systemd/system/<APP_NAME>.service exists over SSH. If it exists, setup is skipped.
Otherwise, the command:
Subsequent deploys skip the setup entirely for speed and safety (unless --force-setup is used).
Note: If you change proxy/TLS/domain settings later, pass --force-setup to re-apply provisioning
changes (e.g., regenerate Caddyfile, adjust firewall rules, rewrite the unit file).
Rollback model
Every deployment that uploads a new binary preserves the previous one as main.prev. A rollback
simply swaps main and main.prev atomically and restarts the service. Non-binary assets (.env,
public, storage, resources) are not rolled back by this command.
Build & artifacts (local)
The command builds the binary (name: APP_NAME) using the configured target OS/ARCH and static
linking preference. See Goravel docs for compiling guidance, artifacts, and what to upload:
https://www.goravel.dev/getting-started/compile.html
Configuration (env)
Required:
app.name: Application name (used in remote paths/service name)DEPLOY_IP_ADDRESS: Target server IPDEPLOY_APP_PORT: Backend app port when reverse proxy is used (e.g. 9000)DEPLOY_SSH_PORT: SSH port (e.g. 22)DEPLOY_SSH_USER: SSH username (user must have sudo privileges)DEPLOY_SSH_KEY_PATH: Path to SSH private key (e.g. ~/.ssh/id_rsa)DEPLOY_OS: Target OS for build (e.g. linux)DEPLOY_ARCH: Target arch for build (e.g. amd64)DEPLOY_PROD_ENV_FILE_PATH: Local path to production .env file to uploadOptional / boolean flags (default false if unset):
DEPLOY_STATIC: Build statically when trueDEPLOY_REVERSE_PROXY_ENABLED: Use Caddy reverse proxy when trueDEPLOY_REVERSE_PROXY_TLS_ENABLED: Enable TLS (requires domain) when trueDEPLOY_DOMAIN: Domain name for TLS or HTTP vhost when using Caddy(required only if TLS is enabled)
CLI flags
--only: Comma-separated subset to deploy: main,env,public,storage,resources-r,--rollback: Rollback to previous binary-F,--force-setup: Force re-run of provisioning even if already set upSecurity & firewall
The command uses SSH with StrictHostKeyChecking=no for convenience. For production, consider
manually trusting the host key to avoid MITM risks. Firewall rules are applied via ufw with
safe ordering: allow OpenSSH and required HTTP(S) ports first, then enable ufw to avoid losing
SSH connectivity.
Systemd service
The unit runs under DEPLOY_SSH_USER. Environment variables are provided via the unit for host/port,
and the working directory points to /var/www/<APP_NAME>. Service restarts are used (brief downtime).
For zero-downtime swaps, a more advanced process manager or socket activation would be required.
High-level deployment flow
Known limitations
Usage examples
Usage example (1 - with reverse proxy):
Assuming you have the following .env file stored in the root of your project as .env.production:
You can then deploy your application to the server with the following command:
This will:
Usage example (2 - without reverse proxy):
You can also deploy without a reverse proxy by setting the DEPLOY_REVERSE_PROXY_ENABLED environment variable to false. For example,
assuming you have the following .env file stored in the root of your project as .env.production and you want to deploy your application to the server without a reverse proxy:
You can then deploy your application to the server with the following command:
This will:
Usage example (3 - rollback):
You can also rollback a deployment to the previous binary by running the following command:
Usage example (4 - force setup):
You can also force the setup of the server by running the following command:
Usage example (5 - only deploy subset of files):
You can also deploy only a subset of the files (such as only the main binary and the environment file) by running the following command:
✅ Checks
All server tests were done on Digital Ocean droplets that were on Ubuntu version 24.04 . However, any server service (AWS, GCP, Hertzner, etc.) that supports connecting to a Debian/Ubuntu server over SSH will work as well.