Riyad Mahrez had a penalty saved but Manchester City still sealed top spot in their Champions League group with a draw that also took Borussia Dortmund into the last 16.
Mahrez was fouled by Emre Can and stepped up himself, but Gregor Kobel went the right way to save his kick.
Both sides knew a draw would be enough to achieve their goals, so each will be happy with the result.
City boss Pep Guardiola surprisingly replaced top scorer Erling Haaland at half-time after just 13 touches in a half in which Mats Hummels dealt with him expertly.
Dortmund had good chances too, with 17-year-old Youssoufa Moukoko slotting wide and debutant City keeper Stefan Ortega making several saves.
City get job done but below best again
City will be happy to have won their Champions League group with a game to spare, but it is just one win in their past four games in all competitions now.
Last time out in Europe they drew 0-0 in Copenhagen, and then lost their unbeaten record to Liverpool in the league - before beating Brighton on Saturday.
Guardiola mixed things up with his line-up in Dortmund, giving a debut to 29-year-old keeper Ortega, a summer signing, with Ederson left on the bench.
The German did well to deny Karim Adeyemi and Moukoko in the first half.
Julian Alvarez, who played behind Haaland, made his fourth start for the club, and was one of only three players to test Kobel - along with Ilkay Gundogan and Mahrez with his failed penalty, all in the second half.
Nathan Ake headed a decent chance over too.
Mahrez has now missed three of his last four penalties for City in all competitions, after scoring nine of his first 10.
City have failed to convert 25 of their 80 penalties under Guardiola. No Premier League team have missed more penalties in that time.
Maybe if Haaland, with 22 goals in his first three months at City, was still on, he might have taken it.
But he was replaced after a quiet first half. Perhaps Guardiola is saving him for the game at struggling Leicester at the weekend.
Joao Cancelo was taken off too, another player who will not be happy with his first half - with Adeyemi getting the better of him on several occasions out wide.
It is all ultimately academic, with City topping the group amid a relentless pre-World Cup fixture list - but recent performances continue to dip below their early-season swashbuckling form.
Analysis
Former Scotland forward James McFadden on BBC Radio 5 Live
It was really competitive, Dortmund really tried to frustrate City and it was very impressive. They offered a real threat to go and win the game in the first half.
City just couldn't get in behind the defence and they couldn't find the way to open Dortmund up. I think it was a very good penalty from Mahrez. It's a side foot with a lot of power, what a save.
Maybe both sides didn't take risks because they knew they didn't have to win the game.
Player of the match
BellinghamJude Bellingham
Borussia Dortmund
Squad number22Player nameBellingham
Squad number15Player nameHummels
Squad number19Player nameBrandt
Squad number23Player nameCan
Squad number1Player nameKobel
Squad number18Player nameMoukoko
Squad number10Player nameT Hazard
Squad number4Player nameSchlotterbeck
Squad number25Player nameSüle
Squad number7Player nameReyna
Squad number27Player nameAdeyemi
Squad number20Player nameModeste
Squad number17Player nameWolf
Squad number47Player namePapadopoulos
Squad number21Player nameMalen
Line-ups
B Dortmund
Formation 4-5-1
1Kobel
25Süle15Hummels4Schlotterbeck10T Hazard
27Adeyemi22Bellingham23Can19Brandt7Reyna
18Moukoko
1Kobel25Süle15HummelsBooked at 87mins4Schlotterbeck10T HazardSubstituted forWolfat 82'minutes27AdeyemiSubstituted forMalenat 73'minutes22Bellingham23Can19Brandt7ReynaSubstituted forPapadopoulosat 87'minutes18MoukokoSubstituted forModesteat 82'minutesSubstitutes
17Wolf20Modeste21Malen30Passlack33Meyer36Rothe38Unbehaun44Coulibaly47PapadopoulosMan City
Formation 4-4-2
18Ortega
5Stones3Rúben Dias6Aké7Cancelo
26Mahrez16Rodri8Gündogan47Foden
9Haaland19Álvarez
18Ortega5Stones3Rúben Dias6Aké7CanceloBooked at 7minsSubstituted forAkanjiat 45'minutesBooked at 72mins26MahrezSubstituted forPalmerat 88'minutes16Rodri8GündoganBooked at 58mins47FodenSubstituted forGrealishat 81'minutes9HaalandSubstituted forBernardo Silvaat 45'minutes19ÁlvarezBooked at 31minsSubstitutes
10Grealish14Laporte17De Bruyne20Bernardo Silva25Akanji31Ederson33Carson80Palmer82Lewis97Wilson-EsbrandReferee:Davide MassaAttendance:81,000Live Text
Match ends, Borussia Dortmund 0, Manchester City 0.
Second Half ends, Borussia Dortmund 0, Manchester City 0.
Offside, Borussia Dortmund. Julian Brandt tries a through ball, but Anthony Modeste is caught offside.
Dangerous play by Rúben Dias (Manchester City).
Jude Bellingham (Borussia Dortmund) wins a free kick in the attacking half.
Substitution, Manchester City. Cole Palmer replaces Riyad Mahrez.
Mats Hummels (Borussia Dortmund) is shown the yellow card for a bad foul.
Julián Álvarez (Manchester City) wins a free kick in the defensive half.
Foul by Mats Hummels (Borussia Dortmund).
Substitution, Borussia Dortmund. Antonios Papadopoulos replaces Giovanni Reyna.
Offside, Manchester City. Jack Grealish tries a through ball, but Bernardo Silva is caught offside.
Offside, Manchester City. John Stones tries a through ball, but Riyad Mahrez is caught offside.
Substitution, Borussia Dortmund. Anthony Modeste replaces Youssoufa Moukoko.
Substitution, Borussia Dortmund. Marius Wolf replaces Thorgan Hazard.
Substitution, Manchester City. Jack Grealish replaces Phil Foden.
Corner, Manchester City. Conceded by Mats Hummels.
Attempt blocked. Thorgan Hazard (Borussia Dortmund) right footed shot from outside the box is blocked.
Substitution, Borussia Dortmund. Donyell Malen replaces Karim Adeyemi.
Manuel Akanji (Manchester City) is shown the yellow card for hand ball.
Hand ball by Manuel Akanji (Manchester City).