[{"data":1,"prerenderedAt":2253},["ShallowReactive",2],{"\u002Fdeploy\u002Fflask-ci-cd-deployment-pipeline-basics":3},{"id":4,"title":5,"body":6,"description":2243,"extension":2244,"meta":2245,"navigation":64,"path":2249,"seo":2250,"stem":2251,"__hash__":2252},"content\u002Fdeploy\u002Fflask-ci-cd-deployment-pipeline-basics.md","Flask CI\u002FCD Deployment Pipeline Basics",{"type":7,"value":8,"toc":2232},"minimark",[9,13,17,22,25,456,459,487,490,507,511,514,517,537,541,1838,1842,1923,1927,1930,1933,2032,2035,2067,2072,2076,2150,2154,2174,2178,2189,2197,2205,2213,2221,2225,2228],[10,11,5],"h1",{"id":12},"flask-cicd-deployment-pipeline-basics",[14,15,16],"p",{},"If you're trying to automate Flask deployments with CI\u002FCD, this guide shows you how to set up a basic pipeline step-by-step. The goal is to run tests on every push, deploy only from the main branch, and safely restart Gunicorn on the server.",[18,19,21],"h2",{"id":20},"quick-fix-quick-setup","Quick Fix \u002F Quick Setup",[14,23,24],{},"Use this GitHub Actions workflow as a working baseline for a Flask app deployed over SSH to a VPS:",[26,27,32],"pre",{"className":28,"code":29,"language":30,"meta":31,"style":31},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Fdeploy.yml\nname: Flask CI\u002FCD\n\non:\n  push:\n    branches: [main]\n\njobs:\n  test-and-deploy:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions\u002Fcheckout@v4\n\n      - name: Set up Python\n        uses: actions\u002Fsetup-python@v5\n        with:\n          python-version: '3.11'\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Run tests\n        run: |\n          pytest -q\n\n      - name: Add SSH key\n        uses: webfactory\u002Fssh-agent@v0.9.0\n        with:\n          ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}\n\n      - name: Trust server host key\n        run: |\n          mkdir -p ~\u002F.ssh\n          ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~\u002F.ssh\u002Fknown_hosts\n\n      - name: Deploy to server\n        run: |\n          ssh ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} '\n            set -e\n            cd \u002Fvar\u002Fwww\u002Fmyapp &&\n            git pull origin main &&\n            source venv\u002Fbin\u002Factivate &&\n            pip install -r requirements.txt &&\n            flask db upgrade &&\n            sudo systemctl restart myapp &&\n            sudo systemctl reload nginx\n          '\n","yaml","",[33,34,35,44,59,66,76,84,99,104,112,120,131,136,144,157,168,173,185,195,203,214,219,231,243,249,255,260,272,281,287,292,304,314,321,332,337,349,358,364,370,375,387,396,402,408,414,420,426,432,438,444,450],"code",{"__ignoreMap":31},[36,37,40],"span",{"class":38,"line":39},"line",1,[36,41,43],{"class":42},"sJ8bj","# .github\u002Fworkflows\u002Fdeploy.yml\n",[36,45,47,51,55],{"class":38,"line":46},2,[36,48,50],{"class":49},"s9eBZ","name",[36,52,54],{"class":53},"sVt8B",": ",[36,56,58],{"class":57},"sZZnC","Flask CI\u002FCD\n",[36,60,62],{"class":38,"line":61},3,[36,63,65],{"emptyLinePlaceholder":64},true,"\n",[36,67,69,73],{"class":38,"line":68},4,[36,70,72],{"class":71},"sj4cs","on",[36,74,75],{"class":53},":\n",[36,77,79,82],{"class":38,"line":78},5,[36,80,81],{"class":49},"  push",[36,83,75],{"class":53},[36,85,87,90,93,96],{"class":38,"line":86},6,[36,88,89],{"class":49},"    branches",[36,91,92],{"class":53},": [",[36,94,95],{"class":57},"main",[36,97,98],{"class":53},"]\n",[36,100,102],{"class":38,"line":101},7,[36,103,65],{"emptyLinePlaceholder":64},[36,105,107,110],{"class":38,"line":106},8,[36,108,109],{"class":49},"jobs",[36,111,75],{"class":53},[36,113,115,118],{"class":38,"line":114},9,[36,116,117],{"class":49},"  test-and-deploy",[36,119,75],{"class":53},[36,121,123,126,128],{"class":38,"line":122},10,[36,124,125],{"class":49},"    runs-on",[36,127,54],{"class":53},[36,129,130],{"class":57},"ubuntu-latest\n",[36,132,134],{"class":38,"line":133},11,[36,135,65],{"emptyLinePlaceholder":64},[36,137,139,142],{"class":38,"line":138},12,[36,140,141],{"class":49},"    steps",[36,143,75],{"class":53},[36,145,147,150,152,154],{"class":38,"line":146},13,[36,148,149],{"class":53},"      - ",[36,151,50],{"class":49},[36,153,54],{"class":53},[36,155,156],{"class":57},"Checkout\n",[36,158,160,163,165],{"class":38,"line":159},14,[36,161,162],{"class":49},"        uses",[36,164,54],{"class":53},[36,166,167],{"class":57},"actions\u002Fcheckout@v4\n",[36,169,171],{"class":38,"line":170},15,[36,172,65],{"emptyLinePlaceholder":64},[36,174,176,178,180,182],{"class":38,"line":175},16,[36,177,149],{"class":53},[36,179,50],{"class":49},[36,181,54],{"class":53},[36,183,184],{"class":57},"Set up Python\n",[36,186,188,190,192],{"class":38,"line":187},17,[36,189,162],{"class":49},[36,191,54],{"class":53},[36,193,194],{"class":57},"actions\u002Fsetup-python@v5\n",[36,196,198,201],{"class":38,"line":197},18,[36,199,200],{"class":49},"        with",[36,202,75],{"class":53},[36,204,206,209,211],{"class":38,"line":205},19,[36,207,208],{"class":49},"          python-version",[36,210,54],{"class":53},[36,212,213],{"class":57},"'3.11'\n",[36,215,217],{"class":38,"line":216},20,[36,218,65],{"emptyLinePlaceholder":64},[36,220,222,224,226,228],{"class":38,"line":221},21,[36,223,149],{"class":53},[36,225,50],{"class":49},[36,227,54],{"class":53},[36,229,230],{"class":57},"Install dependencies\n",[36,232,234,237,239],{"class":38,"line":233},22,[36,235,236],{"class":49},"        run",[36,238,54],{"class":53},[36,240,242],{"class":241},"szBVR","|\n",[36,244,246],{"class":38,"line":245},23,[36,247,248],{"class":57},"          python -m pip install --upgrade pip\n",[36,250,252],{"class":38,"line":251},24,[36,253,254],{"class":57},"          pip install -r requirements.txt\n",[36,256,258],{"class":38,"line":257},25,[36,259,65],{"emptyLinePlaceholder":64},[36,261,263,265,267,269],{"class":38,"line":262},26,[36,264,149],{"class":53},[36,266,50],{"class":49},[36,268,54],{"class":53},[36,270,271],{"class":57},"Run tests\n",[36,273,275,277,279],{"class":38,"line":274},27,[36,276,236],{"class":49},[36,278,54],{"class":53},[36,280,242],{"class":241},[36,282,284],{"class":38,"line":283},28,[36,285,286],{"class":57},"          pytest -q\n",[36,288,290],{"class":38,"line":289},29,[36,291,65],{"emptyLinePlaceholder":64},[36,293,295,297,299,301],{"class":38,"line":294},30,[36,296,149],{"class":53},[36,298,50],{"class":49},[36,300,54],{"class":53},[36,302,303],{"class":57},"Add SSH key\n",[36,305,307,309,311],{"class":38,"line":306},31,[36,308,162],{"class":49},[36,310,54],{"class":53},[36,312,313],{"class":57},"webfactory\u002Fssh-agent@v0.9.0\n",[36,315,317,319],{"class":38,"line":316},32,[36,318,200],{"class":49},[36,320,75],{"class":53},[36,322,324,327,329],{"class":38,"line":323},33,[36,325,326],{"class":49},"          ssh-private-key",[36,328,54],{"class":53},[36,330,331],{"class":57},"${{ secrets.DEPLOY_SSH_KEY }}\n",[36,333,335],{"class":38,"line":334},34,[36,336,65],{"emptyLinePlaceholder":64},[36,338,340,342,344,346],{"class":38,"line":339},35,[36,341,149],{"class":53},[36,343,50],{"class":49},[36,345,54],{"class":53},[36,347,348],{"class":57},"Trust server host key\n",[36,350,352,354,356],{"class":38,"line":351},36,[36,353,236],{"class":49},[36,355,54],{"class":53},[36,357,242],{"class":241},[36,359,361],{"class":38,"line":360},37,[36,362,363],{"class":57},"          mkdir -p ~\u002F.ssh\n",[36,365,367],{"class":38,"line":366},38,[36,368,369],{"class":57},"          ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~\u002F.ssh\u002Fknown_hosts\n",[36,371,373],{"class":38,"line":372},39,[36,374,65],{"emptyLinePlaceholder":64},[36,376,378,380,382,384],{"class":38,"line":377},40,[36,379,149],{"class":53},[36,381,50],{"class":49},[36,383,54],{"class":53},[36,385,386],{"class":57},"Deploy to server\n",[36,388,390,392,394],{"class":38,"line":389},41,[36,391,236],{"class":49},[36,393,54],{"class":53},[36,395,242],{"class":241},[36,397,399],{"class":38,"line":398},42,[36,400,401],{"class":57},"          ssh ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} '\n",[36,403,405],{"class":38,"line":404},43,[36,406,407],{"class":57},"            set -e\n",[36,409,411],{"class":38,"line":410},44,[36,412,413],{"class":57},"            cd \u002Fvar\u002Fwww\u002Fmyapp &&\n",[36,415,417],{"class":38,"line":416},45,[36,418,419],{"class":57},"            git pull origin main &&\n",[36,421,423],{"class":38,"line":422},46,[36,424,425],{"class":57},"            source venv\u002Fbin\u002Factivate &&\n",[36,427,429],{"class":38,"line":428},47,[36,430,431],{"class":57},"            pip install -r requirements.txt &&\n",[36,433,435],{"class":38,"line":434},48,[36,436,437],{"class":57},"            flask db upgrade &&\n",[36,439,441],{"class":38,"line":440},49,[36,442,443],{"class":57},"            sudo systemctl restart myapp &&\n",[36,445,447],{"class":38,"line":446},50,[36,448,449],{"class":57},"            sudo systemctl reload nginx\n",[36,451,453],{"class":38,"line":452},51,[36,454,455],{"class":57},"          '\n",[14,457,458],{},"Replace:",[460,461,462,469,475,481],"ul",{},[463,464,465,468],"li",{},[33,466,467],{},"\u002Fvar\u002Fwww\u002Fmyapp"," with your app path",[463,470,471,474],{},[33,472,473],{},"venv"," with your virtualenv path",[463,476,477,480],{},[33,478,479],{},"myapp"," with your systemd service name",[463,482,483,486],{},[33,484,485],{},"flask db upgrade"," if you do not use Flask-Migrate",[14,488,489],{},"Required GitHub Actions secrets:",[460,491,492,497,502],{},[463,493,494],{},[33,495,496],{},"DEPLOY_HOST",[463,498,499],{},[33,500,501],{},"DEPLOY_USER",[463,503,504],{},[33,505,506],{},"DEPLOY_SSH_KEY",[18,508,510],{"id":509},"whats-happening","What’s Happening",[14,512,513],{},"CI runs automated checks after a git event such as a push or pull request. CD performs deployment actions after those checks pass. For Flask, the basic path is: install dependencies, run tests, connect to the server, update code, install packages, run migrations, restart Gunicorn, and verify service health.",[14,515,516],{},"Most failures happen at the boundaries:",[460,518,519,522,525,528,531,534],{},[463,520,521],{},"SSH authentication",[463,523,524],{},"wrong deployment path",[463,526,527],{},"missing environment variables",[463,529,530],{},"broken virtualenv path",[463,532,533],{},"invalid systemd service configuration",[463,535,536],{},"failed database migrations",[18,538,540],{"id":539},"step-by-step-guide","Step-by-Step Guide",[542,543,544,586,660,714,801,879,914,1246,1340,1418,1507,1635,1689,1755,1797,1836],"ol",{},[463,545,546,550,553,554,556,557,577,579,580,585],{},[547,548,549],"strong",{},"Make sure manual deployment already works",[551,552],"br",{},"CI\u002FCD should automate a known-good deployment path. Before adding automation, confirm you can deploy manually on the server.",[551,555],{},"Minimum server stack:",[460,558,559,562,565,568,571,574],{},[463,560,561],{},"Python",[463,563,564],{},"Git",[463,566,567],{},"virtualenv",[463,569,570],{},"Gunicorn",[463,572,573],{},"Nginx",[463,575,576],{},"systemd service for the app",[551,578],{},"If that is not set up yet, start with ",[581,582,584],"a",{"href":583},"\u002Fdeploy\u002Fdeploy-flask-with-nginx-plus-gunicorn-step-by-step-guide","Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)",".",[463,587,588,591,593,594,631,633,634],{},[547,589,590],{},"Create or confirm the deployment directory",[551,592],{},"Use a fixed path for the application:",[26,595,599],{"className":596,"code":597,"language":598,"meta":31,"style":31},"language-bash shiki shiki-themes github-light github-dark","sudo mkdir -p \u002Fvar\u002Fwww\u002Fmyapp\nsudo chown -R deploy:deploy \u002Fvar\u002Fwww\u002Fmyapp\n","bash",[33,600,601,616],{"__ignoreMap":31},[36,602,603,607,610,613],{"class":38,"line":39},[36,604,606],{"class":605},"sScJk","sudo",[36,608,609],{"class":57}," mkdir",[36,611,612],{"class":71}," -p",[36,614,615],{"class":57}," \u002Fvar\u002Fwww\u002Fmyapp\n",[36,617,618,620,623,626,629],{"class":38,"line":46},[36,619,606],{"class":605},[36,621,622],{"class":57}," chown",[36,624,625],{"class":71}," -R",[36,627,628],{"class":57}," deploy:deploy",[36,630,615],{"class":57},[551,632],{},"Keep your virtualenv in a stable location:",[26,635,637],{"className":596,"code":636,"language":598,"meta":31,"style":31},"cd \u002Fvar\u002Fwww\u002Fmyapp\npython3 -m venv venv\n",[33,638,639,646],{"__ignoreMap":31},[36,640,641,644],{"class":38,"line":39},[36,642,643],{"class":71},"cd",[36,645,615],{"class":57},[36,647,648,651,654,657],{"class":38,"line":46},[36,649,650],{"class":605},"python3",[36,652,653],{"class":71}," -m",[36,655,656],{"class":57}," venv",[36,658,659],{"class":57}," venv\n",[463,661,662,665,667,668,685,687,688,700,702,703],{},[547,663,664],{},"Create a deploy user with limited access",[551,666],{},"Your deploy user should be able to:",[460,669,670,673,676,679,682],{},[463,671,672],{},"access the app directory",[463,674,675],{},"run git pull",[463,677,678],{},"activate the virtualenv",[463,680,681],{},"restart the app service",[463,683,684],{},"optionally reload Nginx",[551,686],{},"Example sudoers rule:",[26,689,691],{"className":596,"code":690,"language":598,"meta":31,"style":31},"sudo visudo\n",[33,692,693],{"__ignoreMap":31},[36,694,695,697],{"class":38,"line":39},[36,696,606],{"class":605},[36,698,699],{"class":57}," visudo\n",[551,701],{},"Add:",[26,704,708],{"className":705,"code":706,"language":707,"meta":31,"style":31},"language-sudoers shiki shiki-themes github-light github-dark","deploy ALL=NOPASSWD: \u002Fbin\u002Fsystemctl restart myapp, \u002Fbin\u002Fsystemctl reload nginx, \u002Fbin\u002Fsystemctl status myapp, \u002Fbin\u002Fsystemctl status nginx\n","sudoers",[33,709,710],{"__ignoreMap":31},[36,711,712],{"class":38,"line":39},[36,713,706],{},[463,715,716,719,721,722,794,796,797,585],{},[547,717,718],{},"Verify the Flask app works directly on the server",[551,720],{},"Run the exact commands that the pipeline will later automate:",[26,723,725],{"className":596,"code":724,"language":598,"meta":31,"style":31},"cd \u002Fvar\u002Fwww\u002Fmyapp\nsource venv\u002Fbin\u002Factivate\npip install -r requirements.txt\nflask db upgrade\nsudo systemctl restart myapp\nsudo systemctl status myapp --no-pager\n",[33,726,727,733,741,755,766,779],{"__ignoreMap":31},[36,728,729,731],{"class":38,"line":39},[36,730,643],{"class":71},[36,732,615],{"class":57},[36,734,735,738],{"class":38,"line":46},[36,736,737],{"class":71},"source",[36,739,740],{"class":57}," venv\u002Fbin\u002Factivate\n",[36,742,743,746,749,752],{"class":38,"line":61},[36,744,745],{"class":605},"pip",[36,747,748],{"class":57}," install",[36,750,751],{"class":71}," -r",[36,753,754],{"class":57}," requirements.txt\n",[36,756,757,760,763],{"class":38,"line":68},[36,758,759],{"class":605},"flask",[36,761,762],{"class":57}," db",[36,764,765],{"class":57}," upgrade\n",[36,767,768,770,773,776],{"class":38,"line":78},[36,769,606],{"class":605},[36,771,772],{"class":57}," systemctl",[36,774,775],{"class":57}," restart",[36,777,778],{"class":57}," myapp\n",[36,780,781,783,785,788,791],{"class":38,"line":86},[36,782,606],{"class":605},[36,784,772],{"class":57},[36,786,787],{"class":57}," status",[36,789,790],{"class":57}," myapp",[36,792,793],{"class":71}," --no-pager\n",[551,795],{},"If your app depends on runtime environment variables, configure them first. See ",[581,798,800],{"href":799},"\u002Fdeploy\u002Fflask-environment-variables-and-secrets-setup","Flask Environment Variables and Secrets Setup",[463,802,803,806,808,809,837,839,840,859,861,862,865,866],{},[547,804,805],{},"Generate an SSH key pair for deployment",[551,807],{},"On your local machine or a secure admin host:",[26,810,812],{"className":596,"code":811,"language":598,"meta":31,"style":31},"ssh-keygen -t ed25519 -C \"github-actions-deploy\" -f deploy_key\n",[33,813,814],{"__ignoreMap":31},[36,815,816,819,822,825,828,831,834],{"class":38,"line":39},[36,817,818],{"class":605},"ssh-keygen",[36,820,821],{"class":71}," -t",[36,823,824],{"class":57}," ed25519",[36,826,827],{"class":71}," -C",[36,829,830],{"class":57}," \"github-actions-deploy\"",[36,832,833],{"class":71}," -f",[36,835,836],{"class":57}," deploy_key\n",[551,838],{},"Copy the public key to the deploy user account on the server:",[26,841,843],{"className":596,"code":842,"language":598,"meta":31,"style":31},"ssh-copy-id -i deploy_key.pub deploy@your-server\n",[33,844,845],{"__ignoreMap":31},[36,846,847,850,853,856],{"class":38,"line":39},[36,848,849],{"class":605},"ssh-copy-id",[36,851,852],{"class":71}," -i",[36,854,855],{"class":57}," deploy_key.pub",[36,857,858],{"class":57}," deploy@your-server\n",[551,860],{},"Or manually append ",[33,863,864],{},"deploy_key.pub"," to:",[26,867,869],{"className":596,"code":868,"language":598,"meta":31,"style":31},"~\u002F.ssh\u002Fauthorized_keys\n",[33,870,871],{"__ignoreMap":31},[36,872,873,876],{"class":38,"line":39},[36,874,875],{"class":241},"~",[36,877,878],{"class":53},"\u002F.ssh\u002Fauthorized_keys\n",[463,880,881,884,886,887,901,903,905,906],{},[547,882,883],{},"Store deployment secrets in GitHub",[551,885],{},"In your repository settings, add these Actions secrets:",[460,888,889,893,897],{},[463,890,891],{},[33,892,496],{},[463,894,895],{},[33,896,501],{},[463,898,899],{},[33,900,506],{},[551,902],{},[33,904,506],{}," should contain the full private key content, including header and footer:",[26,907,912],{"className":908,"code":910,"language":911,"meta":31},[909],"language-text","-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----\n","text",[33,913,910],{"__ignoreMap":31},[463,915,916,919,921,922,931,933,934],{},[547,917,918],{},"Create the workflow file",[551,920],{},"Save this as:",[26,923,925],{"className":596,"code":924,"language":598,"meta":31,"style":31},".github\u002Fworkflows\u002Fdeploy.yml\n",[33,926,927],{"__ignoreMap":31},[36,928,929],{"class":38,"line":39},[36,930,924],{"class":605},[551,932],{},"Use this baseline:",[26,935,937],{"className":28,"code":936,"language":30,"meta":31,"style":31},"name: Flask CI\u002FCD\n\non:\n  push:\n    branches: [main]\n\njobs:\n  test-and-deploy:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions\u002Fcheckout@v4\n\n      - name: Set up Python\n        uses: actions\u002Fsetup-python@v5\n        with:\n          python-version: \"3.11\"\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Run tests\n        run: |\n          pytest -q\n\n      - name: Add SSH key\n        uses: webfactory\u002Fssh-agent@v0.9.0\n        with:\n          ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}\n\n      - name: Trust server host key\n        run: |\n          mkdir -p ~\u002F.ssh\n          ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~\u002F.ssh\u002Fknown_hosts\n\n      - name: Deploy to server\n        run: |\n          ssh ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} '\n            set -e\n            cd \u002Fvar\u002Fwww\u002Fmyapp &&\n            git pull origin main &&\n            source venv\u002Fbin\u002Factivate &&\n            pip install -r requirements.txt &&\n            flask db upgrade &&\n            sudo systemctl restart myapp &&\n            sudo systemctl reload nginx\n          '\n",[33,938,939,947,951,957,963,973,977,983,989,997,1001,1007,1017,1025,1029,1039,1047,1053,1062,1066,1076,1084,1088,1092,1096,1106,1114,1118,1122,1132,1140,1146,1154,1158,1168,1176,1180,1184,1188,1198,1206,1210,1214,1218,1222,1226,1230,1234,1238,1242],{"__ignoreMap":31},[36,940,941,943,945],{"class":38,"line":39},[36,942,50],{"class":49},[36,944,54],{"class":53},[36,946,58],{"class":57},[36,948,949],{"class":38,"line":46},[36,950,65],{"emptyLinePlaceholder":64},[36,952,953,955],{"class":38,"line":61},[36,954,72],{"class":71},[36,956,75],{"class":53},[36,958,959,961],{"class":38,"line":68},[36,960,81],{"class":49},[36,962,75],{"class":53},[36,964,965,967,969,971],{"class":38,"line":78},[36,966,89],{"class":49},[36,968,92],{"class":53},[36,970,95],{"class":57},[36,972,98],{"class":53},[36,974,975],{"class":38,"line":86},[36,976,65],{"emptyLinePlaceholder":64},[36,978,979,981],{"class":38,"line":101},[36,980,109],{"class":49},[36,982,75],{"class":53},[36,984,985,987],{"class":38,"line":106},[36,986,117],{"class":49},[36,988,75],{"class":53},[36,990,991,993,995],{"class":38,"line":114},[36,992,125],{"class":49},[36,994,54],{"class":53},[36,996,130],{"class":57},[36,998,999],{"class":38,"line":122},[36,1000,65],{"emptyLinePlaceholder":64},[36,1002,1003,1005],{"class":38,"line":133},[36,1004,141],{"class":49},[36,1006,75],{"class":53},[36,1008,1009,1011,1013,1015],{"class":38,"line":138},[36,1010,149],{"class":53},[36,1012,50],{"class":49},[36,1014,54],{"class":53},[36,1016,156],{"class":57},[36,1018,1019,1021,1023],{"class":38,"line":146},[36,1020,162],{"class":49},[36,1022,54],{"class":53},[36,1024,167],{"class":57},[36,1026,1027],{"class":38,"line":159},[36,1028,65],{"emptyLinePlaceholder":64},[36,1030,1031,1033,1035,1037],{"class":38,"line":170},[36,1032,149],{"class":53},[36,1034,50],{"class":49},[36,1036,54],{"class":53},[36,1038,184],{"class":57},[36,1040,1041,1043,1045],{"class":38,"line":175},[36,1042,162],{"class":49},[36,1044,54],{"class":53},[36,1046,194],{"class":57},[36,1048,1049,1051],{"class":38,"line":187},[36,1050,200],{"class":49},[36,1052,75],{"class":53},[36,1054,1055,1057,1059],{"class":38,"line":197},[36,1056,208],{"class":49},[36,1058,54],{"class":53},[36,1060,1061],{"class":57},"\"3.11\"\n",[36,1063,1064],{"class":38,"line":205},[36,1065,65],{"emptyLinePlaceholder":64},[36,1067,1068,1070,1072,1074],{"class":38,"line":216},[36,1069,149],{"class":53},[36,1071,50],{"class":49},[36,1073,54],{"class":53},[36,1075,230],{"class":57},[36,1077,1078,1080,1082],{"class":38,"line":221},[36,1079,236],{"class":49},[36,1081,54],{"class":53},[36,1083,242],{"class":241},[36,1085,1086],{"class":38,"line":233},[36,1087,248],{"class":57},[36,1089,1090],{"class":38,"line":245},[36,1091,254],{"class":57},[36,1093,1094],{"class":38,"line":251},[36,1095,65],{"emptyLinePlaceholder":64},[36,1097,1098,1100,1102,1104],{"class":38,"line":257},[36,1099,149],{"class":53},[36,1101,50],{"class":49},[36,1103,54],{"class":53},[36,1105,271],{"class":57},[36,1107,1108,1110,1112],{"class":38,"line":262},[36,1109,236],{"class":49},[36,1111,54],{"class":53},[36,1113,242],{"class":241},[36,1115,1116],{"class":38,"line":274},[36,1117,286],{"class":57},[36,1119,1120],{"class":38,"line":283},[36,1121,65],{"emptyLinePlaceholder":64},[36,1123,1124,1126,1128,1130],{"class":38,"line":289},[36,1125,149],{"class":53},[36,1127,50],{"class":49},[36,1129,54],{"class":53},[36,1131,303],{"class":57},[36,1133,1134,1136,1138],{"class":38,"line":294},[36,1135,162],{"class":49},[36,1137,54],{"class":53},[36,1139,313],{"class":57},[36,1141,1142,1144],{"class":38,"line":306},[36,1143,200],{"class":49},[36,1145,75],{"class":53},[36,1147,1148,1150,1152],{"class":38,"line":316},[36,1149,326],{"class":49},[36,1151,54],{"class":53},[36,1153,331],{"class":57},[36,1155,1156],{"class":38,"line":323},[36,1157,65],{"emptyLinePlaceholder":64},[36,1159,1160,1162,1164,1166],{"class":38,"line":334},[36,1161,149],{"class":53},[36,1163,50],{"class":49},[36,1165,54],{"class":53},[36,1167,348],{"class":57},[36,1169,1170,1172,1174],{"class":38,"line":339},[36,1171,236],{"class":49},[36,1173,54],{"class":53},[36,1175,242],{"class":241},[36,1177,1178],{"class":38,"line":351},[36,1179,363],{"class":57},[36,1181,1182],{"class":38,"line":360},[36,1183,369],{"class":57},[36,1185,1186],{"class":38,"line":366},[36,1187,65],{"emptyLinePlaceholder":64},[36,1189,1190,1192,1194,1196],{"class":38,"line":372},[36,1191,149],{"class":53},[36,1193,50],{"class":49},[36,1195,54],{"class":53},[36,1197,386],{"class":57},[36,1199,1200,1202,1204],{"class":38,"line":377},[36,1201,236],{"class":49},[36,1203,54],{"class":53},[36,1205,242],{"class":241},[36,1207,1208],{"class":38,"line":389},[36,1209,401],{"class":57},[36,1211,1212],{"class":38,"line":398},[36,1213,407],{"class":57},[36,1215,1216],{"class":38,"line":404},[36,1217,413],{"class":57},[36,1219,1220],{"class":38,"line":410},[36,1221,419],{"class":57},[36,1223,1224],{"class":38,"line":416},[36,1225,425],{"class":57},[36,1227,1228],{"class":38,"line":422},[36,1229,431],{"class":57},[36,1231,1232],{"class":38,"line":428},[36,1233,437],{"class":57},[36,1235,1236],{"class":38,"line":434},[36,1237,443],{"class":57},[36,1239,1240],{"class":38,"line":440},[36,1241,449],{"class":57},[36,1243,1244],{"class":38,"line":446},[36,1245,455],{"class":57},[463,1247,1248,1251,1253,1254,1267,1269,1270,1337,1339],{},[547,1249,1250],{},"Run tests before deploy",[551,1252],{},"At minimum:",[26,1255,1257],{"className":596,"code":1256,"language":598,"meta":31,"style":31},"pytest -q\n",[33,1258,1259],{"__ignoreMap":31},[36,1260,1261,1264],{"class":38,"line":39},[36,1262,1263],{"class":605},"pytest",[36,1265,1266],{"class":71}," -q\n",[551,1268],{},"If tests need environment variables, inject test-safe values in the workflow:",[26,1271,1273],{"className":28,"code":1272,"language":30,"meta":31,"style":31},"- name: Run tests\n  env:\n    FLASK_ENV: testing\n    SECRET_KEY: test-secret\n    DATABASE_URL: sqlite:\u002F\u002F\u002Ftest.db\n  run: |\n    pytest -q\n",[33,1274,1275,1286,1293,1303,1313,1323,1332],{"__ignoreMap":31},[36,1276,1277,1280,1282,1284],{"class":38,"line":39},[36,1278,1279],{"class":53},"- ",[36,1281,50],{"class":49},[36,1283,54],{"class":53},[36,1285,271],{"class":57},[36,1287,1288,1291],{"class":38,"line":46},[36,1289,1290],{"class":49},"  env",[36,1292,75],{"class":53},[36,1294,1295,1298,1300],{"class":38,"line":61},[36,1296,1297],{"class":49},"    FLASK_ENV",[36,1299,54],{"class":53},[36,1301,1302],{"class":57},"testing\n",[36,1304,1305,1308,1310],{"class":38,"line":68},[36,1306,1307],{"class":49},"    SECRET_KEY",[36,1309,54],{"class":53},[36,1311,1312],{"class":57},"test-secret\n",[36,1314,1315,1318,1320],{"class":38,"line":78},[36,1316,1317],{"class":49},"    DATABASE_URL",[36,1319,54],{"class":53},[36,1321,1322],{"class":57},"sqlite:\u002F\u002F\u002Ftest.db\n",[36,1324,1325,1328,1330],{"class":38,"line":86},[36,1326,1327],{"class":49},"  run",[36,1329,54],{"class":53},[36,1331,242],{"class":241},[36,1333,1334],{"class":38,"line":101},[36,1335,1336],{"class":57},"    pytest -q\n",[551,1338],{},"Do not reuse production secrets for test execution unless strictly required.",[463,1341,1342,1345,1347,1348,1350,1351,1378,1380,1381,1415,1417],{},[547,1343,1344],{},"Restrict deployment to the production branch",[551,1346],{},"For a basic pipeline, deploy only from ",[33,1349,95],{},":",[26,1352,1354],{"className":28,"code":1353,"language":30,"meta":31,"style":31},"on:\n  push:\n    branches: [main]\n",[33,1355,1356,1362,1368],{"__ignoreMap":31},[36,1357,1358,1360],{"class":38,"line":39},[36,1359,72],{"class":71},[36,1361,75],{"class":53},[36,1363,1364,1366],{"class":38,"line":46},[36,1365,81],{"class":49},[36,1367,75],{"class":53},[36,1369,1370,1372,1374,1376],{"class":38,"line":61},[36,1371,89],{"class":49},[36,1373,92],{"class":53},[36,1375,95],{"class":57},[36,1377,98],{"class":53},[551,1379],{},"If you want tests on pull requests too:",[26,1382,1384],{"className":28,"code":1383,"language":30,"meta":31,"style":31},"on:\n  push:\n    branches: [main]\n  pull_request:\n",[33,1385,1386,1392,1398,1408],{"__ignoreMap":31},[36,1387,1388,1390],{"class":38,"line":39},[36,1389,72],{"class":71},[36,1391,75],{"class":53},[36,1393,1394,1396],{"class":38,"line":46},[36,1395,81],{"class":49},[36,1397,75],{"class":53},[36,1399,1400,1402,1404,1406],{"class":38,"line":61},[36,1401,89],{"class":49},[36,1403,92],{"class":53},[36,1405,95],{"class":57},[36,1407,98],{"class":53},[36,1409,1410,1413],{"class":38,"line":68},[36,1411,1412],{"class":49},"  pull_request",[36,1414,75],{"class":53},[551,1416],{},"Then split deploy into a branch-guarded job.",[463,1419,1420,1423,1425,1426,1487,1489,1490],{},[547,1421,1422],{},"Use a predictable remote deploy sequence",[551,1424],{},"Keep the remote command sequence simple and atomic:",[26,1427,1429],{"className":596,"code":1428,"language":598,"meta":31,"style":31},"ssh deploy@your-server '\n  set -e\n  cd \u002Fvar\u002Fwww\u002Fmyapp &&\n  git pull origin main &&\n  source venv\u002Fbin\u002Factivate &&\n  pip install -r requirements.txt &&\n  flask db upgrade &&\n  sudo systemctl restart myapp &&\n  sudo systemctl reload nginx\n'\n",[33,1430,1431,1442,1447,1452,1457,1462,1467,1472,1477,1482],{"__ignoreMap":31},[36,1432,1433,1436,1439],{"class":38,"line":39},[36,1434,1435],{"class":605},"ssh",[36,1437,1438],{"class":57}," deploy@your-server",[36,1440,1441],{"class":57}," '\n",[36,1443,1444],{"class":38,"line":46},[36,1445,1446],{"class":57},"  set -e\n",[36,1448,1449],{"class":38,"line":61},[36,1450,1451],{"class":57},"  cd \u002Fvar\u002Fwww\u002Fmyapp &&\n",[36,1453,1454],{"class":38,"line":68},[36,1455,1456],{"class":57},"  git pull origin main &&\n",[36,1458,1459],{"class":38,"line":78},[36,1460,1461],{"class":57},"  source venv\u002Fbin\u002Factivate &&\n",[36,1463,1464],{"class":38,"line":86},[36,1465,1466],{"class":57},"  pip install -r requirements.txt &&\n",[36,1468,1469],{"class":38,"line":101},[36,1470,1471],{"class":57},"  flask db upgrade &&\n",[36,1473,1474],{"class":38,"line":106},[36,1475,1476],{"class":57},"  sudo systemctl restart myapp &&\n",[36,1478,1479],{"class":38,"line":114},[36,1480,1481],{"class":57},"  sudo systemctl reload nginx\n",[36,1483,1484],{"class":38,"line":122},[36,1485,1486],{"class":57},"'\n",[551,1488],{},"Important points:",[460,1491,1492,1498,1501],{},[463,1493,1494,1497],{},[33,1495,1496],{},"set -e"," stops on first failure",[463,1499,1500],{},"absolute paths reduce ambiguity",[463,1502,1503,1506],{},[33,1504,1505],{},"systemctl"," should manage Gunicorn, not background shell commands",[463,1508,1509,1512,1514,1515,1590,1592,1593,1628,1630,1631,585],{},[547,1510,1511],{},"Confirm your systemd service is correct",[551,1513],{},"Example Flask Gunicorn service:",[26,1516,1520],{"className":1517,"code":1518,"language":1519,"meta":31,"style":31},"language-ini shiki shiki-themes github-light github-dark","# \u002Fetc\u002Fsystemd\u002Fsystem\u002Fmyapp.service\n[Unit]\nDescription=Gunicorn instance for myapp\nAfter=network.target\n\n[Service]\nUser=www-data\nGroup=www-data\nWorkingDirectory=\u002Fvar\u002Fwww\u002Fmyapp\nEnvironmentFile=\u002Fetc\u002Fmyapp.env\nExecStart=\u002Fvar\u002Fwww\u002Fmyapp\u002Fvenv\u002Fbin\u002Fgunicorn --workers 3 --bind unix:\u002Frun\u002Fmyapp.sock wsgi:app\n\n[Install]\nWantedBy=multi-user.target\n","ini",[33,1521,1522,1527,1532,1537,1542,1546,1551,1556,1561,1566,1571,1576,1580,1585],{"__ignoreMap":31},[36,1523,1524],{"class":38,"line":39},[36,1525,1526],{},"# \u002Fetc\u002Fsystemd\u002Fsystem\u002Fmyapp.service\n",[36,1528,1529],{"class":38,"line":46},[36,1530,1531],{},"[Unit]\n",[36,1533,1534],{"class":38,"line":61},[36,1535,1536],{},"Description=Gunicorn instance for myapp\n",[36,1538,1539],{"class":38,"line":68},[36,1540,1541],{},"After=network.target\n",[36,1543,1544],{"class":38,"line":78},[36,1545,65],{"emptyLinePlaceholder":64},[36,1547,1548],{"class":38,"line":86},[36,1549,1550],{},"[Service]\n",[36,1552,1553],{"class":38,"line":101},[36,1554,1555],{},"User=www-data\n",[36,1557,1558],{"class":38,"line":106},[36,1559,1560],{},"Group=www-data\n",[36,1562,1563],{"class":38,"line":114},[36,1564,1565],{},"WorkingDirectory=\u002Fvar\u002Fwww\u002Fmyapp\n",[36,1567,1568],{"class":38,"line":122},[36,1569,1570],{},"EnvironmentFile=\u002Fetc\u002Fmyapp.env\n",[36,1572,1573],{"class":38,"line":133},[36,1574,1575],{},"ExecStart=\u002Fvar\u002Fwww\u002Fmyapp\u002Fvenv\u002Fbin\u002Fgunicorn --workers 3 --bind unix:\u002Frun\u002Fmyapp.sock wsgi:app\n",[36,1577,1578],{"class":38,"line":138},[36,1579,65],{"emptyLinePlaceholder":64},[36,1581,1582],{"class":38,"line":146},[36,1583,1584],{},"[Install]\n",[36,1586,1587],{"class":38,"line":159},[36,1588,1589],{},"WantedBy=multi-user.target\n",[551,1591],{},"Reload and enable if you change it:",[26,1594,1596],{"className":596,"code":1595,"language":598,"meta":31,"style":31},"sudo systemctl daemon-reload\nsudo systemctl enable myapp\nsudo systemctl restart myapp\n",[33,1597,1598,1607,1618],{"__ignoreMap":31},[36,1599,1600,1602,1604],{"class":38,"line":39},[36,1601,606],{"class":605},[36,1603,772],{"class":57},[36,1605,1606],{"class":57}," daemon-reload\n",[36,1608,1609,1611,1613,1616],{"class":38,"line":46},[36,1610,606],{"class":605},[36,1612,772],{"class":57},[36,1614,1615],{"class":57}," enable",[36,1617,778],{"class":57},[36,1619,1620,1622,1624,1626],{"class":38,"line":61},[36,1621,606],{"class":605},[36,1623,772],{"class":57},[36,1625,775],{"class":57},[36,1627,778],{"class":57},[551,1629],{},"For a full service setup, see ",[581,1632,1634],{"href":1633},"\u002Fdeploy\u002Fflask-systemd-plus-gunicorn-service-setup","Flask systemd + Gunicorn Service Setup",[463,1636,1637,1640,1642,1643,1645,1646,1671,1673,1674],{},[547,1638,1639],{},"Keep production secrets on the server",[551,1641],{},"Runtime secrets usually should not live in the CI workflow. Keep them in an environment file or service configuration on the server.",[551,1644],{},"Example:",[26,1647,1649],{"className":1517,"code":1648,"language":1519,"meta":31,"style":31},"# \u002Fetc\u002Fmyapp.env\nFLASK_ENV=production\nSECRET_KEY=replace-this\nDATABASE_URL=postgresql:\u002F\u002Fuser:pass@localhost\u002Fmyapp\n",[33,1650,1651,1656,1661,1666],{"__ignoreMap":31},[36,1652,1653],{"class":38,"line":39},[36,1654,1655],{},"# \u002Fetc\u002Fmyapp.env\n",[36,1657,1658],{"class":38,"line":46},[36,1659,1660],{},"FLASK_ENV=production\n",[36,1662,1663],{"class":38,"line":61},[36,1664,1665],{},"SECRET_KEY=replace-this\n",[36,1667,1668],{"class":38,"line":68},[36,1669,1670],{},"DATABASE_URL=postgresql:\u002F\u002Fuser:pass@localhost\u002Fmyapp\n",[551,1672],{},"Then restart the app:",[26,1675,1677],{"className":596,"code":1676,"language":598,"meta":31,"style":31},"sudo systemctl restart myapp\n",[33,1678,1679],{"__ignoreMap":31},[36,1680,1681,1683,1685,1687],{"class":38,"line":39},[36,1682,606],{"class":605},[36,1684,772],{"class":57},[36,1686,775],{"class":57},[36,1688,778],{"class":57},[463,1690,1691,1694,1696,1697,1723,1725,1726],{},[547,1692,1693],{},"Add a post-deploy health check",[551,1695],{},"Verify the app responds after restart:",[26,1698,1700],{"className":596,"code":1699,"language":598,"meta":31,"style":31},"curl -I https:\u002F\u002Fyourdomain.com\ncurl -fsS https:\u002F\u002Fyourdomain.com\u002Fhealth\n",[33,1701,1702,1713],{"__ignoreMap":31},[36,1703,1704,1707,1710],{"class":38,"line":39},[36,1705,1706],{"class":605},"curl",[36,1708,1709],{"class":71}," -I",[36,1711,1712],{"class":57}," https:\u002F\u002Fyourdomain.com\n",[36,1714,1715,1717,1720],{"class":38,"line":46},[36,1716,1706],{"class":605},[36,1718,1719],{"class":71}," -fsS",[36,1721,1722],{"class":57}," https:\u002F\u002Fyourdomain.com\u002Fhealth\n",[551,1724],{},"You can add this directly to the workflow:",[26,1727,1729],{"className":28,"code":1728,"language":30,"meta":31,"style":31},"- name: Health check\n  run: |\n    curl -fsS https:\u002F\u002Fyourdomain.com\u002Fhealth\n",[33,1730,1731,1742,1750],{"__ignoreMap":31},[36,1732,1733,1735,1737,1739],{"class":38,"line":39},[36,1734,1279],{"class":53},[36,1736,50],{"class":49},[36,1738,54],{"class":53},[36,1740,1741],{"class":57},"Health check\n",[36,1743,1744,1746,1748],{"class":38,"line":46},[36,1745,1327],{"class":49},[36,1747,54],{"class":53},[36,1749,242],{"class":241},[36,1751,1752],{"class":38,"line":61},[36,1753,1754],{"class":57},"    curl -fsS https:\u002F\u002Fyourdomain.com\u002Fhealth\n",[463,1756,1757,1760,1762,1763,1790,1792,1793,585],{},[547,1758,1759],{},"Validate Nginx only when needed",[551,1761],{},"If your deploy includes Nginx config changes, validate before reload:",[26,1764,1766],{"className":596,"code":1765,"language":598,"meta":31,"style":31},"sudo nginx -t\nsudo systemctl reload nginx\n",[33,1767,1768,1778],{"__ignoreMap":31},[36,1769,1770,1772,1775],{"class":38,"line":39},[36,1771,606],{"class":605},[36,1773,1774],{"class":57}," nginx",[36,1776,1777],{"class":71}," -t\n",[36,1779,1780,1782,1784,1787],{"class":38,"line":46},[36,1781,606],{"class":605},[36,1783,772],{"class":57},[36,1785,1786],{"class":57}," reload",[36,1788,1789],{"class":57}," nginx\n",[551,1791],{},"If Nginx is misconfigured, deploy may appear to succeed while traffic still fails. See ",[581,1794,1796],{"href":1795},"\u002Ffix-issues\u002Ffix-flask-502-bad-gateway-step-by-step-guide","Fix Flask 502 Bad Gateway (Step-by-Step Guide)",[463,1798,1799,1802,1804,1805,1819,1821,1822],{},[547,1800,1801],{},"Keep rollback simple",[551,1803],{},"For a basic VPS pipeline, rollback discipline means:",[460,1806,1807,1810,1813,1816],{},[463,1808,1809],{},"deploy small changes",[463,1811,1812],{},"keep commits clean",[463,1814,1815],{},"confirm migrations are safe",[463,1817,1818],{},"inspect logs before retrying failed restarts",[551,1820],{},"Advanced release models can later move to:",[460,1823,1824,1827,1830,1833],{},[463,1825,1826],{},"tagged releases",[463,1828,1829],{},"immutable artifacts",[463,1831,1832],{},"blue\u002Fgreen deployment",[463,1834,1835],{},"staged approval gates",[463,1837],{},[18,1839,1841],{"id":1840},"common-causes","Common Causes",[460,1843,1844,1857,1863,1873,1881,1891,1900,1917],{},[463,1845,1846,1849,1850,1852,1853,1856],{},[547,1847,1848],{},"SSH authentication failure"," → CI cannot connect to the server → verify ",[33,1851,506],{},", ",[33,1854,1855],{},"authorized_keys",", and host key trust.",[463,1858,1859,1862],{},[547,1860,1861],{},"Wrong working directory"," → remote commands run outside the app path → use an absolute deployment directory in the SSH command.",[463,1864,1865,1868,1869,1872],{},[547,1866,1867],{},"Missing virtualenv activation"," → packages or Flask CLI are unavailable → activate the correct venv before ",[33,1870,1871],{},"pip install"," and migrations.",[463,1874,1875,1878,1879,585],{},[547,1876,1877],{},"Deploy user lacks sudo for systemctl"," → Gunicorn restart fails → allow only the required service commands in ",[33,1880,707],{},[463,1882,1883,1886,1887,1890],{},[547,1884,1885],{},"Environment variables missing on the server"," → app starts in CI tests but fails in production → verify the ",[33,1888,1889],{},"EnvironmentFile"," or service environment settings.",[463,1892,1893,1896,1897,1899],{},[547,1894,1895],{},"Migration command fails"," → schema is behind or database URL is wrong → test ",[33,1898,485],{}," manually on the server first.",[463,1901,1902,1905,1906,1852,1909,1912,1913,1916],{},[547,1903,1904],{},"Service unit misconfiguration"," → restart succeeds poorly or app crashes immediately → inspect ",[33,1907,1908],{},"ExecStart",[33,1910,1911],{},"WorkingDirectory",", and ",[33,1914,1915],{},"User"," in the unit file.",[463,1918,1919,1922],{},[547,1920,1921],{},"Nginx reload masks app failure"," → proxy stays up but backend is down → check Gunicorn and app logs, not only Nginx status.",[18,1924,1926],{"id":1925},"debugging","Debugging",[14,1928,1929],{},"Check the failing stage in order: CI logs, SSH access, remote commands, Gunicorn status, then Nginx status.",[14,1931,1932],{},"Commands to run:",[26,1934,1936],{"className":596,"code":1935,"language":598,"meta":31,"style":31},"pytest -q\nssh deploy@your-server 'cd \u002Fvar\u002Fwww\u002Fmyapp && git status && git pull origin main'\nssh deploy@your-server 'cd \u002Fvar\u002Fwww\u002Fmyapp && source venv\u002Fbin\u002Factivate && pip install -r requirements.txt'\nssh deploy@your-server 'cd \u002Fvar\u002Fwww\u002Fmyapp && source venv\u002Fbin\u002Factivate && flask db upgrade'\nssh deploy@your-server 'sudo systemctl status myapp --no-pager'\nssh deploy@your-server 'sudo journalctl -u myapp -n 100 --no-pager'\nssh deploy@your-server 'sudo nginx -t'\nssh deploy@your-server 'sudo systemctl status nginx --no-pager'\nssh deploy@your-server 'sudo tail -n 100 \u002Fvar\u002Flog\u002Fnginx\u002Ferror.log'\ncurl -I https:\u002F\u002Fyourdomain.com\ncurl -fsS https:\u002F\u002Fyourdomain.com\u002Fhealth\n",[33,1937,1938,1944,1953,1962,1971,1980,1989,1998,2007,2016,2024],{"__ignoreMap":31},[36,1939,1940,1942],{"class":38,"line":39},[36,1941,1263],{"class":605},[36,1943,1266],{"class":71},[36,1945,1946,1948,1950],{"class":38,"line":46},[36,1947,1435],{"class":605},[36,1949,1438],{"class":57},[36,1951,1952],{"class":57}," 'cd \u002Fvar\u002Fwww\u002Fmyapp && git status && git pull origin main'\n",[36,1954,1955,1957,1959],{"class":38,"line":61},[36,1956,1435],{"class":605},[36,1958,1438],{"class":57},[36,1960,1961],{"class":57}," 'cd \u002Fvar\u002Fwww\u002Fmyapp && source venv\u002Fbin\u002Factivate && pip install -r requirements.txt'\n",[36,1963,1964,1966,1968],{"class":38,"line":68},[36,1965,1435],{"class":605},[36,1967,1438],{"class":57},[36,1969,1970],{"class":57}," 'cd \u002Fvar\u002Fwww\u002Fmyapp && source venv\u002Fbin\u002Factivate && flask db upgrade'\n",[36,1972,1973,1975,1977],{"class":38,"line":78},[36,1974,1435],{"class":605},[36,1976,1438],{"class":57},[36,1978,1979],{"class":57}," 'sudo systemctl status myapp --no-pager'\n",[36,1981,1982,1984,1986],{"class":38,"line":86},[36,1983,1435],{"class":605},[36,1985,1438],{"class":57},[36,1987,1988],{"class":57}," 'sudo journalctl -u myapp -n 100 --no-pager'\n",[36,1990,1991,1993,1995],{"class":38,"line":101},[36,1992,1435],{"class":605},[36,1994,1438],{"class":57},[36,1996,1997],{"class":57}," 'sudo nginx -t'\n",[36,1999,2000,2002,2004],{"class":38,"line":106},[36,2001,1435],{"class":605},[36,2003,1438],{"class":57},[36,2005,2006],{"class":57}," 'sudo systemctl status nginx --no-pager'\n",[36,2008,2009,2011,2013],{"class":38,"line":114},[36,2010,1435],{"class":605},[36,2012,1438],{"class":57},[36,2014,2015],{"class":57}," 'sudo tail -n 100 \u002Fvar\u002Flog\u002Fnginx\u002Ferror.log'\n",[36,2017,2018,2020,2022],{"class":38,"line":122},[36,2019,1706],{"class":605},[36,2021,1709],{"class":71},[36,2023,1712],{"class":57},[36,2025,2026,2028,2030],{"class":38,"line":133},[36,2027,1706],{"class":605},[36,2029,1719],{"class":71},[36,2031,1722],{"class":57},[14,2033,2034],{},"What to look for:",[460,2036,2037,2043,2049,2055,2061],{},[463,2038,2039,2042],{},[547,2040,2041],{},"CI logs:"," package install failures, test failures, malformed SSH key, missing secret values",[463,2044,2045,2048],{},[547,2046,2047],{},"SSH session:"," wrong user, denied key, host verification failure",[463,2050,2051,2054],{},[547,2052,2053],{},"Gunicorn\u002Fsystemd logs:"," import errors, missing env vars, permission problems, bad socket path",[463,2056,2057,2060],{},[547,2058,2059],{},"Nginx logs:"," upstream connect errors, socket mismatch, invalid config",[463,2062,2063,2066],{},[547,2064,2065],{},"Migration step:"," incorrect database URL, missing migration scripts, app import failures",[14,2068,2069,2070,585],{},"If requests still fail after deploy, use ",[581,2071,1796],{"href":1795},[18,2073,2075],{"id":2074},"checklist","Checklist",[460,2077,2080,2089,2102,2108,2114,2120,2126,2132,2138,2144],{"className":2078},[2079],"contains-task-list",[463,2081,2084,2088],{"className":2082},[2083],"task-list-item",[2085,2086],"input",{"disabled":64,"type":2087},"checkbox"," Manual deployment works on the server before CI\u002FCD is enabled.",[463,2090,2092,2094,2095,1852,2097,1912,2099,2101],{"className":2091},[2083],[2085,2093],{"disabled":64,"type":2087}," ",[33,2096,496],{},[33,2098,501],{},[33,2100,506],{}," are stored as CI secrets.",[463,2103,2105,2107],{"className":2104},[2083],[2085,2106],{"disabled":64,"type":2087}," The deploy user can access the app directory and restart the Flask service.",[463,2109,2111,2113],{"className":2110},[2083],[2085,2112],{"disabled":64,"type":2087}," The workflow runs tests before deployment.",[463,2115,2117,2119],{"className":2116},[2083],[2085,2118],{"disabled":64,"type":2087}," Deployment is restricted to the production branch or release trigger.",[463,2121,2123,2125],{"className":2122},[2083],[2085,2124],{"disabled":64,"type":2087}," The app virtualenv path is correct on the server.",[463,2127,2129,2131],{"className":2128},[2083],[2085,2130],{"disabled":64,"type":2087}," Database migrations run successfully if included in the pipeline.",[463,2133,2135,2137],{"className":2134},[2083],[2085,2136],{"disabled":64,"type":2087}," systemd restarts Gunicorn without errors.",[463,2139,2141,2143],{"className":2140},[2083],[2085,2142],{"disabled":64,"type":2087}," Nginx reloads cleanly if included in the deploy step.",[463,2145,2147,2149],{"className":2146},[2083],[2085,2148],{"disabled":64,"type":2087}," A post-deploy health check returns HTTP 200 or expected headers.",[18,2151,2153],{"id":2152},"related-guides","Related Guides",[460,2155,2156,2160,2164,2168],{},[463,2157,2158],{},[581,2159,584],{"href":583},[463,2161,2162],{},[581,2163,800],{"href":799},[463,2165,2166],{},[581,2167,1796],{"href":1795},[463,2169,2170],{},[581,2171,2173],{"href":2172},"\u002Fchecklist\u002Fflask-production-checklist-everything-you-must-do","Flask Production Checklist (Everything You Must Do)",[18,2175,2177],{"id":2176},"faq","FAQ",[14,2179,2180,2183,2185,2186,2188],{},[547,2181,2182],{},"Q: What is the minimum useful CI\u002FCD pipeline for Flask?",[551,2184],{},"\nA: Run tests on push, deploy only from ",[33,2187,95],{},", update code on the server, install dependencies, run migrations, restart Gunicorn, and verify a health check.",[14,2190,2191,2194,2196],{},[547,2192,2193],{},"Q: Should I use GitHub Actions for Flask CI\u002FCD?",[551,2195],{},"\nA: Yes. It is a common baseline for VPS deployments and works well with SSH-based deploys.",[14,2198,2199,2202,2204],{},[547,2200,2201],{},"Q: Do I need Docker for CI\u002FCD?",[551,2203],{},"\nA: No. A Flask app can use CI\u002FCD with a standard virtualenv, systemd, and Nginx setup.",[14,2206,2207,2210,2212],{},[547,2208,2209],{},"Q: Where should production secrets live?",[551,2211],{},"\nA: Usually on the server in systemd environment files or equivalent runtime configuration, not in the workflow unless required for deployment.",[14,2214,2215,2218,2220],{},[547,2216,2217],{},"Q: How do I know the deploy actually worked?",[551,2219],{},"\nA: Check the CI job status, systemd service status, application logs, and a post-deploy HTTP health check.",[18,2222,2224],{"id":2223},"final-takeaway","Final Takeaway",[14,2226,2227],{},"A basic Flask CI\u002FCD pipeline should automate an existing manual deployment path: test, connect, update code, install dependencies, run migrations, restart Gunicorn, and verify health. Keep the first version simple, predictable, and easy to debug before adding more advanced release logic.",[2229,2230,2231],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":31,"searchDepth":46,"depth":46,"links":2233},[2234,2235,2236,2237,2238,2239,2240,2241,2242],{"id":20,"depth":46,"text":21},{"id":509,"depth":46,"text":510},{"id":539,"depth":46,"text":540},{"id":1840,"depth":46,"text":1841},{"id":1925,"depth":46,"text":1926},{"id":2074,"depth":46,"text":2075},{"id":2152,"depth":46,"text":2153},{"id":2176,"depth":46,"text":2177},{"id":2223,"depth":46,"text":2224},"Complete guide on flask ci\u002Fcd deployment pipeline basics for Flask production environments.","md",{"ogTitle":5,"ogDescription":2243,"twitterCard":2246,"robots":2247,"canonical":2248},"summary_large_image","index, follow","https:\u002F\u002Fflask-deployment.com\u002Fdeploy\u002Fflask-ci-cd-deployment-pipeline-basics","\u002Fdeploy\u002Fflask-ci-cd-deployment-pipeline-basics",{"title":5,"description":2243},"deploy\u002Fflask-ci-cd-deployment-pipeline-basics","10jkexzTXGn2SJqkqkGSCznUwjew0C7OkfTRVVdsyLk",1776805765065]